diff --git a/.changeset/session-wakeups.md b/.changeset/session-wakeups.md new file mode 100644 index 000000000..b1f47e47f --- /dev/null +++ b/.changeset/session-wakeups.md @@ -0,0 +1,5 @@ +--- +'@roomote/web': minor +--- + +Fast Sessions can schedule a message to themselves with the new `manage_wakeups` tool. Ask Fast to "remind me in twenty minutes" or "check every ten minutes whether CI is green" and it creates a wakeup for that Session; when it fires, Fast picks the conversation back up with its history in context, does what was asked, and replies on the surface the Session lives on. One-shot wakeups always reply, recurring ones stay quiet unless there is news and cancel themselves when the monitored condition resolves. Wakeups are scoped to the conversation, need no administrator, are capped at ten per Session, and are cancelled when the Session is archived. Firing is durable: each occurrence is claimed on the row, a delayed queue job is only a hint, and a recovery sweep re-adds lost hints. diff --git a/apps/bullmq/src/index.ts b/apps/bullmq/src/index.ts index be81d6950..d17e204ce 100644 --- a/apps/bullmq/src/index.ts +++ b/apps/bullmq/src/index.ts @@ -54,6 +54,7 @@ import { startTaskSleepQueue } from './task-sleep-queue'; import { startAutomationRecommendationsQueue } from './automation-recommendations-queue'; import { startFastAgentParentEventQueue } from './fast-agent-parent-event-queue'; import { readBullMqQueueHealth } from './health'; +import { startSessionWakeupQueue } from './session-wakeup-queue'; import { installBullMqGracefulShutdown } from './graceful-shutdown'; // Deployments roll every service at once while migrations run only ahead @@ -216,6 +217,11 @@ const { worker: fastAgentParentEventWorker, queueEvents: fastAgentParentEventQueueEvents, } = await startFastAgentParentEventQueue(); +const { + queue: sessionWakeupQueue, + worker: sessionWakeupWorker, + queueEvents: sessionWakeupQueueEvents, +} = await startSessionWakeupQueue(); const serverAdapter = new HonoAdapter(serveStatic); @@ -256,6 +262,7 @@ createBullBoard({ readOnlyMode: false, }), new BullMQAdapter(fastAgentParentEventQueue, { readOnlyMode: false }), + new BullMQAdapter(sessionWakeupQueue, { readOnlyMode: false }), ], serverAdapter, }); @@ -448,6 +455,9 @@ installBullMqGracefulShutdown({ await pullRequestMergeabilityCheckQueue.close(); await fastAgentParentEventQueueEvents.close(); await fastAgentParentEventQueue.close(); + await sessionWakeupWorker.close(); + await sessionWakeupQueueEvents.close(); + await sessionWakeupQueue.close(); await discordGatewaySupervisor.stop(); await closeRedis(); }, diff --git a/apps/bullmq/src/session-wakeup-queue.ts b/apps/bullmq/src/session-wakeup-queue.ts new file mode 100644 index 000000000..d177764ec --- /dev/null +++ b/apps/bullmq/src/session-wakeup-queue.ts @@ -0,0 +1,87 @@ +import { Queue, QueueEvents, Worker, type Job } from 'bullmq'; + +import { + SESSION_WAKEUP_FIRE_JOB_NAME, + SESSION_WAKEUP_QUEUE_NAME, + fireSessionWakeup, + recoverPendingSessionWakeups, + type SessionWakeupFireJob, +} from '@roomote/sdk/server'; + +import { getRedis } from './redis'; + +const RECOVERY_JOB_NAME = 'recover-due'; +const RECOVERY_SCHEDULER_ID = 'session-wakeup-recovery'; +const RECOVERY_INTERVAL_MS = 60_000; + +type SessionWakeupQueueJob = SessionWakeupFireJob | { recovery: true }; + +async function processJob(job: Job) { + if (job.name === RECOVERY_JOB_NAME || 'recovery' in job.data) { + const recovered = await recoverPendingSessionWakeups(); + if (recovered > 0) { + console.log( + `[SessionWakeupQueue] Re-added ${recovered} due wakeup hint(s).`, + ); + } + return; + } + if (job.name !== SESSION_WAKEUP_FIRE_JOB_NAME) return; + const result = await fireSessionWakeup(job.data); + if (result.outcome === 'skipped') { + console.log( + `[SessionWakeupQueue] Skipped wakeup ${job.data.wakeupId}: ${result.reason}`, + ); + } +} + +/** + * Fires session wakeups. Every job is a hint for a `session_wakeups` row: + * the row's `next_run_at` decides whether anything happens, and a recovery + * sweep re-adds hints for due rows so lost jobs cannot strand a wakeup. + */ +export async function startSessionWakeupQueue() { + const connection = getRedis(); + const queue = new Queue(SESSION_WAKEUP_QUEUE_NAME, { + connection, + defaultJobOptions: { + attempts: 3, + backoff: { type: 'exponential', delay: 2_000 }, + removeOnComplete: true, + removeOnFail: true, + }, + }); + + await queue.upsertJobScheduler( + RECOVERY_SCHEDULER_ID, + { every: RECOVERY_INTERVAL_MS }, + { name: RECOVERY_JOB_NAME, data: { recovery: true } }, + ); + await recoverPendingSessionWakeups(); + + const worker = new Worker( + SESSION_WAKEUP_QUEUE_NAME, + processJob, + // Firing only admits an event into the conversation's durable inbox; the + // turn itself runs on the Fast parent event worker. + { connection, concurrency: 10, autorun: true }, + ); + + worker.on('failed', (job, error) => + console.error( + `[SessionWakeupQueue] job ${job?.id} failed: ${error.message}`, + ), + ); + worker.on('error', (error) => + console.error('[SessionWakeupQueue] worker error:', error), + ); + + const queueEvents = new QueueEvents(SESSION_WAKEUP_QUEUE_NAME, { + connection, + }); + queueEvents.on('failed', ({ jobId, failedReason }) => + console.error(`[SessionWakeupQueue] job ${jobId} failed: ${failedReason}`), + ); + + return { queue, worker, queueEvents }; +} diff --git a/apps/docs/fast-sessions.mdx b/apps/docs/fast-sessions.mdx index 5ca77f0ad..26b561052 100644 --- a/apps/docs/fast-sessions.mdx +++ b/apps/docs/fast-sessions.mdx @@ -145,6 +145,31 @@ In shared Slack and Discord conversations, Fast can stay silent when linked participants are talking to each other. A direct reply to Roomote, a mention, or a direct message still requires a response. +## Reminders and monitors + +A Session can schedule a message to itself. Ask Fast to "remind me in twenty +minutes", "check every ten minutes whether CI is green", or "every weekday at +nine, summarize the open pull requests", and it creates a wakeup for that +Session. When the wakeup fires, Fast picks the conversation back up with its +full history in context, does what was asked, and replies on the same surface +the Session lives on: the transcript in the dashboard, or the originating Slack, +Discord, Teams, or Telegram thread. + +A one-shot wakeup is a reminder and always replies. A recurring wakeup is a +monitor and stays quiet unless it has something to report, such as a change, a +result, a blocker, or a decision you need to make. When the monitored condition +resolves, Fast tells you and cancels the wakeup itself. You can also ask Fast to +list or cancel wakeups at any time; archiving a Session cancels all of its +wakeups. + +Wakeups belong to the conversation and do not require an administrator. A +Session may hold up to ten active wakeups, intervals range from one minute to +seven days, and recurring wakeups can be bounded by a run count or an end time. +Intervals under five minutes must carry one of those bounds. A recurring wakeup +whose turns fail five times in a row is retired. Deployment-wide recurring work +that should run outside a conversation or report to a channel is an +[automation](/automations) instead. + ## Session and execution access Any signed-in deployment user with a Session link can view its timeline and diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/tool-presentation.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/tool-presentation.ts index afcab26b7..6b7443c51 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/tool-presentation.ts +++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/tool-presentation.ts @@ -104,6 +104,7 @@ const COMMUNICATION_TOOL_NAMES = new Set([ ]); const TOOL_ICON_OVERRIDES: Readonly>> = { manage_custom_automations: 'task', + manage_wakeups: 'task', get_about_me: 'roomote', describe_video: 'video', manage_goal: 'target', diff --git a/apps/web/src/trpc/commands/sessions/archive.test.ts b/apps/web/src/trpc/commands/sessions/archive.test.ts new file mode 100644 index 000000000..34b819b4a --- /dev/null +++ b/apps/web/src/trpc/commands/sessions/archive.test.ts @@ -0,0 +1,232 @@ +import { + db, + fastAgentConversations, + sessionFactory, + userFactory, +} from '@roomote/db/server'; +import { + acquireFastAgentTurnLock, + fastAgentConversationRepository, +} from '@roomote/cloud-agents/server'; +import * as sessionQueries from '@/lib/server/sessions'; +import type { UserAuthSuccess } from '@/types'; +import { archiveSessionCommand } from './index'; + +const mocks = vi.hoisted(() => ({ + acquireRedisLock: vi.fn(), + cancelWakeups: vi.fn(), +})); + +vi.mock('@roomote/redis', async (importOriginal) => ({ + ...(await importOriginal()), + acquireRedisLock: mocks.acquireRedisLock, +})); +vi.mock('@roomote/db/server', async (importOriginal) => ({ + ...(await importOriginal()), + cancelSessionWakeupsForConversation: mocks.cancelWakeups, +})); +vi.mock('@roomote/sdk/server', () => ({ + syncFastAgentSlackTitleBestEffort: vi.fn(), +})); +vi.mock('@roomote/telemetry/server', () => ({ captureEvent: vi.fn() })); + +describe('archiveSessionCommand turn serialization', () => { + const locks = new Set(); + let contention: ReturnType>; + + beforeEach(() => { + locks.clear(); + contention = Promise.withResolvers(); + mocks.cancelWakeups.mockReset().mockResolvedValue(0); + mocks.acquireRedisLock + .mockReset() + .mockImplementation(async (key: string) => { + if (locks.has(key)) { + contention.resolve(); + return null; + } + locks.add(key); + return Object.assign( + async () => { + locks.delete(key); + }, + { + renewDetailed: async () => 'renewed', + }, + ); + }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + expect(locks.size).toBe(0); + }); + + async function fixture(fast = true) { + const owner = await userFactory.create(); + const [record] = fast + ? 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, + fastConversationId: record?.id ?? null, + }); + const auth = { userId: owner.id, isAdmin: false } as UserAuthSuccess; + return { auth, session, record }; + } + + it('cannot commit archive while reply is in flight, then archives after the turn releases', async () => { + const { auth, session, record } = await fixture(); + const conversation = await fastAgentConversationRepository.findById({ + id: record!.id, + }); + vi.useFakeTimers({ + toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], + }); + const turn = await acquireFastAgentTurnLock({ + conversation: conversation!.conversation, + }); + const reply = Promise.withResolvers(); + const replyFinished = reply.promise.finally(() => turn!()); + const archive = archiveSessionCommand(auth, session.id); + await contention.promise; + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeNull(); + expect(mocks.cancelWakeups).not.toHaveBeenCalled(); + reply.resolve(); + await replyFinished; + await vi.advanceTimersByTimeAsync(500); + expect(await archive).toMatchObject({ + id: session.id, + archivedAt: expect.any(Date), + }); + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeInstanceOf(Date); + expect(mocks.cancelWakeups).toHaveBeenCalledWith(record!.id); + }); + + it('fails retryably without archival when the turn stays busy', async () => { + const { auth, session, record } = await fixture(); + const conversation = await fastAgentConversationRepository.findById({ + id: record!.id, + }); + vi.useFakeTimers({ + toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], + }); + const turn = await acquireFastAgentTurnLock({ + conversation: conversation!.conversation, + }); + try { + const result = expect( + archiveSessionCommand(auth, session.id), + ).rejects.toMatchObject({ code: 'CONFLICT' }); + await contention.promise; + await vi.advanceTimersByTimeAsync(2_000); + await result; + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeNull(); + expect(mocks.cancelWakeups).not.toHaveBeenCalled(); + } finally { + await turn!(); + } + }); + + it('releases the lock when the metadata update fails', async () => { + const { auth, session } = await fixture(); + vi.spyOn(sessionQueries, 'updateSessionMetadata').mockRejectedValueOnce( + new Error('database unavailable'), + ); + await expect(archiveSessionCommand(auth, session.id)).rejects.toThrow( + 'database unavailable', + ); + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeNull(); + expect(locks.size).toBe(0); + expect(await archiveSessionCommand(auth, session.id)).toMatchObject({ + archivedAt: expect.any(Date), + }); + }); + + it('does not archive when lock acquisition fails', async () => { + const { auth, session } = await fixture(); + mocks.acquireRedisLock.mockRejectedValueOnce( + new Error('redis unavailable'), + ); + await expect(archiveSessionCommand(auth, session.id)).rejects.toThrow( + 'redis unavailable', + ); + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeNull(); + expect(mocks.cancelWakeups).not.toHaveBeenCalled(); + }); + + it('releases the lock and preserves archival if wakeup cancellation fails', async () => { + const { auth, session } = await fixture(); + mocks.cancelWakeups.mockRejectedValueOnce(new Error('cancel failed')); + vi.spyOn(console, 'error').mockImplementation(() => {}); + expect(await archiveSessionCommand(auth, session.id)).toMatchObject({ + archivedAt: expect.any(Date), + }); + }); + + it('checks archive permission before conversation lookup or locking and preserves missing behavior', async () => { + const { auth, session } = await fixture(); + const stranger = await userFactory.create(); + const lookup = vi.spyOn(fastAgentConversationRepository, 'findById'); + expect( + await archiveSessionCommand({ ...auth, userId: stranger.id }, session.id), + ).toBeNull(); + expect(await archiveSessionCommand(auth, crypto.randomUUID())).toBeNull(); + expect(lookup).not.toHaveBeenCalled(); + expect(mocks.acquireRedisLock).not.toHaveBeenCalled(); + expect( + await archiveSessionCommand( + { ...auth, userId: stranger.id, isAdmin: true }, + session.id, + ), + ).toMatchObject({ archivedAt: expect.any(Date) }); + }); + + it('archives task-only sessions without a turn lock', async () => { + const { auth, session } = await fixture(false); + expect(await archiveSessionCommand(auth, session.id)).toMatchObject({ + archivedAt: expect.any(Date), + }); + expect(mocks.acquireRedisLock).not.toHaveBeenCalled(); + }); + + it('fails closed if the conversation cannot be resolved', async () => { + const { auth, session } = await fixture(); + vi.spyOn(fastAgentConversationRepository, 'findById').mockResolvedValueOnce( + null, + ); + await expect(archiveSessionCommand(auth, session.id)).rejects.toMatchObject( + { code: 'CONFLICT' }, + ); + expect( + (await sessionQueries.findAccessibleSession(auth, session.id)) + ?.archivedAt, + ).toBeNull(); + expect(mocks.acquireRedisLock).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/trpc/commands/sessions/index.ts b/apps/web/src/trpc/commands/sessions/index.ts index 96eb423d7..1bee6bb66 100644 --- a/apps/web/src/trpc/commands/sessions/index.ts +++ b/apps/web/src/trpc/commands/sessions/index.ts @@ -1,6 +1,15 @@ import { z } from 'zod'; +import { TRPCError } from '@trpc/server'; +import { + acquireFastAgentTurnLock, + fastAgentConversationRepository, +} from '@roomote/cloud-agents/server'; import { SESSION_STATUSES } from '@roomote/types'; -import { advanceSessionReadCursor, db } from '@roomote/db/server'; +import { + advanceSessionReadCursor, + cancelSessionWakeupsForConversation, + db, +} from '@roomote/db/server'; import { captureEvent } from '@roomote/telemetry/server'; import type { UserAuthSuccess } from '@/types'; @@ -127,16 +136,60 @@ 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' }, + const session = await findAccessibleSession(auth, sessionId); + if (!session || (!auth.isAdmin && session.ownerUserId !== auth.userId)) { + return null; + } + + let releaseTurnLock; + if (session.fastConversationId) { + const record = await fastAgentConversationRepository.findById({ + id: session.fastConversationId, + }); + if (!record) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Session is unavailable. Please try archiving again.', + }); + } + // Serialize with reply delivery, without holding a DB transaction over I/O. + releaseTurnLock = await acquireFastAgentTurnLock({ + conversation: record.conversation, + maxWaitMs: 2_000, + }); + if (!releaseTurnLock) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Session is busy. Please try archiving again shortly.', + }); + } + } + + try { + releaseTurnLock?.signal.throwIfAborted(); + const archived = await updateSessionMetadata(auth, sessionId, { + archivedAt: new Date(), }); + if (archived) { + if (archived.fastConversationId) { + // An archived session must not wake itself up later. + await cancelSessionWakeupsForConversation( + archived.fastConversationId, + ).catch((error) => { + console.error( + `[sessions] Failed to cancel wakeups for archived session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`, + ); + }); + } + void captureEvent('session_archived', { + userId: auth.userId, + properties: { surface: 'web', outcome: 'archived' }, + }); + } + return archived; + } finally { + await releaseTurnLock?.(); } - return archived; } export { diff --git a/packages/cloud-agents/package.json b/packages/cloud-agents/package.json index bb0636afa..9413636e3 100644 --- a/packages/cloud-agents/package.json +++ b/packages/cloud-agents/package.json @@ -79,6 +79,8 @@ "@roomote/telemetry": "workspace:^", "@roomote/types": "workspace:^", "ai": "^6.0.116", + "bullmq": "^5.78.0", + "cron-parser": "5.6.1", "dompurify": "3.4.13", "jsdom": "26.1.0", "jszip": "^3.10.1", diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation.ts index 8af832c86..f944966dd 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation.ts @@ -30,7 +30,8 @@ export type FastAgentPlatformEventKind = | 'delegated_task' | 'automation' | 'setup' - | 'input_response'; + | 'input_response' + | 'scheduled_wakeup'; /** Shared with the durable follow-up event so an admitted reaction resumes as the same input. */ export type FastAgentReactionExternalInput = diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts index 62e07a6eb..e4f1538c2 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts @@ -27,6 +27,11 @@ import { FIND_INTEGRATION_TOOLS_TOOL, INTEGRATION_TOOL_LOOKUP_MAX_LIMIT, REASONING_EFFORT_VALUES, + MANAGE_WAKEUPS_TOOL_DESCRIPTION, + SESSION_WAKEUP_NAME_MAX_LENGTH, + SESSION_WAKEUP_PROMPT_MAX_LENGTH, + SESSION_WAKEUP_SCHEDULE_GRAMMAR, + SESSION_WAKEUP_SCHEDULE_MAX_LENGTH, type FastAgentSurface, FAST_EXECUTION, } from '@roomote/types'; @@ -386,6 +391,24 @@ export default { args: { taskId: z.string().nullable().optional() }, execute: (args, context) => invoke("cancel_task", args, context), } +`, + + [FAST_AGENT_NATIVE_TOOL_NAMES.manageWakeups]: String.raw` +import { z } from "zod" +import { invoke } from "../roomote-fast-tool-bridge.js" + +export default { + description: ${JSON.stringify(MANAGE_WAKEUPS_TOOL_DESCRIPTION)}, + args: { + action: z.enum(["create", "list", "get", "cancel"]).describe("create schedules a wakeup; list shows active wakeups in this conversation; get shows one; cancel stops one. Cancel is the only stop action."), + wakeupId: z.string().optional().describe("Required for get and cancel. Omit otherwise."), + name: z.string().min(3).max(${SESSION_WAKEUP_NAME_MAX_LENGTH}).optional().describe("[create] Short label, e.g. 'Check PR #85 for merge'"), + prompt: z.string().min(10).max(${SESSION_WAKEUP_PROMPT_MAX_LENGTH}).optional().describe("[create] What to do when it fires. This conversation stays in context, so keep it short: what to check, what counts as done, what to tell the user."), + schedule: z.string().max(${SESSION_WAKEUP_SCHEDULE_MAX_LENGTH}).optional().describe(${JSON.stringify(`[create] ${SESSION_WAKEUP_SCHEDULE_GRAMMAR}`)}), + reportPolicy: z.enum(["always", "only_when_notable"]).optional().describe("[create] 'always' replies on every run (default for one-shots); 'only_when_notable' stays silent unless there is news (default for repeating schedules). Omit to use the default."), + }, + execute: (args, context) => invoke("manage_wakeups", args, context), +} `, [FAST_AGENT_NATIVE_TOOL_NAMES.retryTaskStart]: String.raw` diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts index 544ca675e..56030aa96 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts @@ -209,6 +209,7 @@ export function buildFastAgentSystemPrompt({ : ''; const recurringAutomationGuidance = `## Recurring Work and Automations - When an admin explicitly asks for recurring work, recognize a real cadence expression such as "every Monday", "daily", "weekly", "whenever X happens", "from now on", or "on a schedule". Do not treat preference words such as "always use tabs" as a cadence. +- Reminders and recurring checks that belong to this conversation ("remind me in an hour", "check every 10 minutes until CI is green", "ping me here every weekday at 9") are wakeups, not automations: use "manage_wakeups", which needs no admin. Reach for a custom automation only for deployment-wide recurring work that should run outside this conversation or report to a channel. - Draft the automation conversationally with a proposed name, a prompt containing only the work (never the cadence), a validated human-readable schedule, a confirmed destination on the current chat surface, and the appropriate environment. Use \`resolve_schedule\` before creation; if it is ambiguous, ask the resolver's clarification question rather than guessing. - Before \`create\`, use \`list\` to check for an equivalent automation. Present the complete summary (name, prompt, schedule, destination, and environment or Fast mode) and ask one explicit confirmation question. Never create, update, enable, or delete silently. After creation, ask whether the user wants to \`run_now\` to test it. - If the user is not an admin, do not attempt creation. Explain that an administrator is required and provide a copy-pasteable draft name, prompt, and schedule instead. @@ -349,6 +350,7 @@ ${reactionGuidance} - Use "cancel_task" only when the user explicitly asks to stop an active task. - Call a deployment MCP tool when it can answer the request. Fast receives the same actor-authorized remote and deployment-proxied MCP tool catalog as delegated tasks; local stdio servers remain sandbox-only. Servers listed with a tool prefix expose each tool individually with its native JSON schema. On-demand servers are reached through \`find_integration_tools\` (fetch the schema by server id and tool name, or search by keywords) followed by \`call_integration_tool\`; the same acknowledgement, duplicate, and audit rules apply to both paths. - Use \`roomote_manage_custom_automations\` for custom automation lifecycle requests. It uses the current user's deployment authorization, is admin-only, and is unavailable to advisor and judge subagents. List before modifying an existing automation, use "list_models" before setting a model override, use update with "enabled" to enable or disable, and use "run_now" rather than "launch_task" to test an automation. Communicate first on a human-authored turn; platform events remain exempt. Delete only when the user explicitly requests it, and after creating an automation ask whether they want to run it now. +- Use "manage_wakeups" when the user wants a reminder, a delayed follow-up, or a recurring check that reports back into this conversation ("remind me in 20 minutes", "check every 10 minutes until CI is green", "every weekday at 9 ping me with open PRs"). The schedule is one short string: "in 20m" for a reminder, "every 10m" or "every 1m x3" for a repeating check, "cron 0 9 * * 1-5" for a calendar schedule. Send only the fields the action needs. It is scoped to this conversation and available to every participant. Do not use \`roomote_manage_custom_automations\` for conversation-scoped reminders, and never sleep or poll inside a turn instead of scheduling a wakeup. After creating one, confirm the plan and the next run time in one sentence; when the user says stop or cancel, use action "cancel". ${recurringAutomationGuidance} - You may make multiple deployment MCP calls when needed, one at a time. Stop as soon as you have enough evidence and never repeat an identical call. @@ -359,8 +361,8 @@ ${recurringAutomationGuidance} - Select an environment ID only when the target is clear. Otherwise use null to use the deployment default. ${ platformEvent - ? `## ${platformEventKind === 'automation' ? 'Automation Platform Event' : platformEventKind === 'setup' ? 'Setup Platform Event' : platformEventKind === 'input_response' ? 'Structured Input Response Event' : 'Delegated Task Platform Event'} -- The current input is a trusted platform-generated ${platformEventKind === 'automation' ? 'custom automation request' : platformEventKind === 'setup' ? 'setup lifecycle event' : platformEventKind === 'input_response' ? 'structured user-input response' : 'event about a delegated task'}, not a human-authored request. + ? `## ${platformEventKind === 'automation' ? 'Automation Platform Event' : platformEventKind === 'setup' ? 'Setup Platform Event' : platformEventKind === 'input_response' ? 'Structured Input Response Event' : platformEventKind === 'scheduled_wakeup' ? 'Scheduled Wakeup Event' : 'Delegated Task Platform Event'} +- The current input is a trusted platform-generated ${platformEventKind === 'automation' ? 'custom automation request' : platformEventKind === 'setup' ? 'setup lifecycle event' : platformEventKind === 'input_response' ? 'structured user-input response' : platformEventKind === 'scheduled_wakeup' ? 'wakeup this conversation scheduled for itself' : 'event about a delegated task'}, not a human-authored request. ${ platformEventVisibility === 'required' ? '- This event requires one user-visible terminal response because it carries user-useful substance. Present its result, changed expectation, required decision, or recovery action; never narrate lifecycle state alone. Use a closeout unless the setup instructions require `request_user_input`. Do not call "ignore_event".' @@ -377,6 +379,16 @@ ${ ? "- The payload contains the user's submitted structured answers. Persist any needed state, continue the interrupted work with those answers, and acknowledge the choice in one closeout. Do not re-ask the same questions." : '' } +${ + platformEventKind === 'scheduled_wakeup' + ? `- The payload is a wakeup you scheduled earlier in this conversation with "manage_wakeups"; its \`prompt\` says what to do now. The conversation history is still in context, so act on the prompt directly rather than treating it as a new request. +- Do the work the prompt asks for. Use integrations directly when sufficient, and launch a task only when repository or workspace execution is actually required. +- \`reportPolicy\` governs whether to speak. With "always", finish with one closeout addressed to the user. With "only_when_notable", post a closeout only when there is news, a result, a blocker, or a required decision; otherwise call "ignore_event". +- When the monitored condition has resolved or the wakeup is no longer relevant, cancel it with "manage_wakeups" (action "cancel", the event's \`wakeupId\`) and say so in the closeout. \`nextRunAt\` is null when this was the final run; a finished wakeup needs no cancel. +- Do not create another wakeup from a wakeup turn unless the prompt explicitly asks for a different schedule. +` + : '' +} ${ platformEventKind === 'setup' ? '- Setup lifecycle events carry trusted readiness, connection, selection, and recommendation facts. Reconcile them against the setup snapshot, continue the next setup step, and finish with the terminal response required by the setup instructions.' 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 014fd444e..774b68068 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 @@ -20,6 +20,7 @@ import { buildInferenceProviderRecoveryPrompt, fastAgentHumanFollowUpEventSchema, formatErrorForLog, + manageWakeupsInputSchema, resolveInferenceProviderRetryDelayMs, isMemoryMcpServer, truncateAcpOutputText, @@ -57,6 +58,10 @@ import { z } from 'zod'; import packageJson from '../../../../../package.json'; import { appendAttachmentTextsToPromptText } from '../../file-attachments'; +import { + handleManageWakeupsToolCall, + normalizeManageWakeupsArgs, +} from '../session-wakeups'; import { buildSlackThreadPromptBlocks, wrapSlackMessage, @@ -3394,6 +3399,10 @@ export async function answerFastAgentQuestion({ FAST_AGENT_NATIVE_TOOL_NAMES.sendChatReply, FAST_AGENT_NATIVE_TOOL_NAMES.sendChatReaction, FAST_AGENT_NATIVE_TOOL_NAMES.ignoreEvent, + // Scheduling or cancelling a wakeup is instant and its own confirmation + // follows in the closeout; an acknowledgement first would only add a + // second message. + FAST_AGENT_NATIVE_TOOL_NAMES.manageWakeups, // A catalog lookup reads nothing external; the call it prepares for is // still gated on the acknowledgement. FAST_AGENT_NATIVE_TOOL_NAMES.findIntegrationTools, @@ -4201,6 +4210,20 @@ export async function answerFastAgentQuestion({ return result; } + case FAST_AGENT_NATIVE_TOOL_NAMES.manageWakeups: { + const args = manageWakeupsInputSchema.parse( + normalizeManageWakeupsArgs(call.args), + ); + + throwIfTurnCancelled(); + + return await handleManageWakeupsToolCall( + { conversationId: session.id, userId }, + + args, + ); + } + case FAST_AGENT_NATIVE_TOOL_NAMES.retryTaskStart: { if (!platformEvent || !adapter.retryTaskStart) { return { diff --git a/packages/cloud-agents/src/server/index.ts b/packages/cloud-agents/src/server/index.ts index f85cc08f6..c017a95c6 100644 --- a/packages/cloud-agents/src/server/index.ts +++ b/packages/cloud-agents/src/server/index.ts @@ -24,6 +24,7 @@ export * from './automation-root-summary'; export * from './audio-transcription'; export * from './file-attachments'; export * from './fast-agent'; +export * from './session-wakeups'; // Canonical API base URL fallback chain (explicit -> TRPC_URL -> R_APP_URL). // Fast surfaces must derive apiBaseUrl through this so the broker's // deployment-proxy origin check matches the resolver-built proxy URLs. diff --git a/packages/cloud-agents/src/server/session-wakeups/args.test.ts b/packages/cloud-agents/src/server/session-wakeups/args.test.ts new file mode 100644 index 000000000..96fb08da5 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/args.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { manageWakeupsInputSchema } from '@roomote/types'; + +import { normalizeManageWakeupsArgs } from './args'; + +describe('normalizeManageWakeupsArgs', () => { + it('drops empty-string, null, and placeholder values', () => { + expect( + normalizeManageWakeupsArgs({ + action: 'create', + wakeupId: '', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + schedule: 'in 2m', + reportPolicy: null, + }), + ).toEqual({ + action: 'create', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + schedule: 'in 2m', + }); + expect( + normalizeManageWakeupsArgs({ action: 'list', wakeupId: 'none' }), + ).toEqual({ action: 'list' }); + }); + + it('makes a model-shaped create call pass the contract', () => { + const parsed = manageWakeupsInputSchema.parse( + normalizeManageWakeupsArgs({ + action: 'create', + wakeupId: '', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + schedule: 'in 2m', + reportPolicy: '', + }), + ); + expect(parsed.schedule).toBe('in 2m'); + expect(parsed.wakeupId).toBeUndefined(); + expect(parsed.reportPolicy).toBeUndefined(); + }); + + it('keeps real values untouched', () => { + expect( + normalizeManageWakeupsArgs({ + action: 'cancel', + wakeupId: 'abc', + reportPolicy: 'always', + }), + ).toEqual({ action: 'cancel', wakeupId: 'abc', reportPolicy: 'always' }); + }); +}); diff --git a/packages/cloud-agents/src/server/session-wakeups/args.ts b/packages/cloud-agents/src/server/session-wakeups/args.ts new file mode 100644 index 000000000..b3e5bdd00 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/args.ts @@ -0,0 +1,33 @@ +/** + * Models routinely fill every optional tool argument, sending "", null, or a + * literal "none" for the ones they do not mean to use. Dropping those before + * validation keeps the contract honest instead of failing on placeholders + * and burning a retry. + */ +export function normalizeManageWakeupsArgs( + args: Record, +): Record { + return stripEmpty(args) as Record; +} + +const PLACEHOLDER_STRINGS = new Set(['null', 'none', 'undefined', 'n/a']); + +function stripEmpty(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(stripEmpty); + } + if (value && typeof value === 'object') { + const entries = Object.entries(value as Record) + .map(([key, nested]) => [key, stripEmpty(nested)] as const) + .filter(([, nested]) => nested !== undefined); + return Object.fromEntries(entries); + } + if (value === null) return undefined; + if (typeof value === 'string') { + const trimmed = value.trim(); + if (trimmed === '' || PLACEHOLDER_STRINGS.has(trimmed.toLowerCase())) { + return undefined; + } + } + return value; +} diff --git a/packages/cloud-agents/src/server/session-wakeups/index.ts b/packages/cloud-agents/src/server/session-wakeups/index.ts new file mode 100644 index 000000000..ddee3a6c8 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/index.ts @@ -0,0 +1,37 @@ +export { normalizeManageWakeupsArgs } from './args'; +export { + parseSessionWakeupSchedule, + type ParsedSessionWakeupSchedule, +} from './parse'; +export { + SESSION_WAKEUP_FIRE_JOB_NAME, + SESSION_WAKEUP_QUEUE_NAME, + buildSessionWakeupFireJobId, + enqueueSessionWakeupFire, + enqueueSessionWakeupFireBestEffort, + type SessionWakeupFireJob, +} from './queue'; +export { + SessionWakeupValidationError, + computeNextSessionWakeupRunAt, + describeSessionWakeupSchedule, + normalizeSessionWakeupSchedule, + normalizeSessionWakeupTimeZone, + resolveSessionWakeupNextRun, + validateSessionWakeupCaps, + type NormalizedSessionWakeupSchedule, + type SessionWakeupScheduleInput, +} from './schedule'; +export { + cancelSessionWakeupForConversation, + createSessionWakeup, + getSessionWakeupForConversation, + handleManageWakeupsToolCall, + listSessionWakeupsForConversation, + resolveSessionWakeupTimeZone, + toSessionWakeupSummary, + type CancelSessionWakeupResult, + type CreateSessionWakeupInput, + type CreateSessionWakeupResult, + type SessionWakeupActor, +} from './service'; diff --git a/packages/cloud-agents/src/server/session-wakeups/parse.test.ts b/packages/cloud-agents/src/server/session-wakeups/parse.test.ts new file mode 100644 index 000000000..149ed07fd --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/parse.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it } from 'vitest'; + +import { parseSessionWakeupSchedule } from './parse'; +import { SessionWakeupValidationError } from './schedule'; + +const now = new Date('2026-09-04T17:00:00.000Z'); +const options = { now, defaultTimeZone: 'America/New_York' }; + +describe('parseSessionWakeupSchedule', () => { + it('reads one-shot delays in several spellings', () => { + for (const text of ['in 2m', 'in 2 minutes', 'IN 2min', 'once in 2m']) { + const parsed = parseSessionWakeupSchedule(text, options); + expect(parsed.schedule).toEqual({ + mode: 'once', + at: '2026-09-04T17:02:00.000Z', + inMinutes: 2, + }); + expect(parsed.maxRuns).toBeNull(); + expect(parsed.until).toBeNull(); + } + expect( + parseSessionWakeupSchedule('in 3h', options).firstRunAt.toISOString(), + ).toBe('2026-09-04T20:00:00.000Z'); + expect( + parseSessionWakeupSchedule('in 2 days', options).firstRunAt.toISOString(), + ).toBe('2026-09-06T17:00:00.000Z'); + }); + + it('reads an absolute one-shot time', () => { + expect( + parseSessionWakeupSchedule( + 'at 2026-09-04T15:00:00-04:00', + options, + ).firstRunAt.toISOString(), + ).toBe('2026-09-04T19:00:00.000Z'); + }); + + it('keeps relative identity across changed clocks and equivalent units', () => { + const first = parseSessionWakeupSchedule('in 1h', options); + const retry = parseSessionWakeupSchedule('in 60m', { + ...options, + now: new Date(now.getTime() + 5_000), + }); + expect(first.schedule).toEqual({ + mode: 'once', + inMinutes: 60, + at: first.firstRunAt.toISOString(), + }); + expect(retry.schedule).toEqual({ + mode: 'once', + inMinutes: 60, + at: retry.firstRunAt.toISOString(), + }); + expect(retry.firstRunAt.getTime() - first.firstRunAt.getTime()).toBe(5_000); + expect( + parseSessionWakeupSchedule('at 2026-09-04T18:00:00Z', options).schedule, + ).toEqual({ mode: 'once', at: first.firstRunAt.toISOString() }); + }); + + it('reads intervals with run counts and end times in either order', () => { + const plain = parseSessionWakeupSchedule('every 10m', options); + expect(plain.schedule).toEqual({ mode: 'interval', everyMinutes: 10 }); + expect(plain.maxRuns).toBeNull(); + + for (const text of [ + 'every 1m x3', + 'every 1m x 3', + 'every 1 minute 3 times', + 'every minute for 3 runs', + ]) { + const parsed = parseSessionWakeupSchedule(text, options); + expect(parsed.schedule).toEqual({ mode: 'interval', everyMinutes: 1 }); + expect(parsed.maxRuns).toBe(3); + } + + const both = parseSessionWakeupSchedule( + 'every 10m until 2026-09-04T18:00:00Z x5', + options, + ); + expect(both.maxRuns).toBe(5); + expect(both.until?.toISOString()).toBe('2026-09-04T18:00:00.000Z'); + expect( + parseSessionWakeupSchedule('every 6 hours', options).schedule, + ).toEqual({ mode: 'interval', everyMinutes: 360 }); + }); + + it('reads cron with an optional timezone', () => { + expect( + parseSessionWakeupSchedule('cron 0 9 * * 1-5', options).schedule, + ).toEqual({ + mode: 'cron', + expression: '0 9 * * 1-5', + timezone: 'America/New_York', + }); + expect( + parseSessionWakeupSchedule('cron 0 9 * * 1-5 Europe/Berlin', options) + .schedule, + ).toEqual({ + mode: 'cron', + expression: '0 9 * * 1-5', + timezone: 'Europe/Berlin', + }); + }); + + it('holds high-frequency cron to the same cap as intervals', () => { + expect(() => parseSessionWakeupSchedule('cron * * * * *', options)).toThrow( + /Cron schedules that fire more often/, + ); + expect(() => + parseSessionWakeupSchedule('cron */2 * * * * UTC', options), + ).toThrow(/Cron schedules that fire more often/); + const bounded = parseSessionWakeupSchedule('cron * * * * * x3', options); + expect(bounded.maxRuns).toBe(3); + expect(bounded.schedule).toEqual({ + mode: 'cron', + expression: '* * * * *', + timezone: 'America/New_York', + }); + const withTz = parseSessionWakeupSchedule( + 'cron * * * * * UTC until 2026-09-04T18:00:00Z', + options, + ); + expect(withTz.schedule).toEqual({ + mode: 'cron', + expression: '* * * * *', + timezone: 'UTC', + }); + expect(withTz.until?.toISOString()).toBe('2026-09-04T18:00:00.000Z'); + expect(() => + parseSessionWakeupSchedule('cron */10 * * * *', options), + ).not.toThrow(); + expect(() => + parseSessionWakeupSchedule('cron 0 9 * * 1-5 Europe/Berlin x2', options), + ).not.toThrow(); + }); + + it('enforces the tight-interval cap through the string form', () => { + expect(() => parseSessionWakeupSchedule('every 1m', options)).toThrow( + /run count|end time/, + ); + expect(() => + parseSessionWakeupSchedule('every 1m x3', options), + ).not.toThrow(); + }); + + it('rejects unreadable schedules with the grammar in the message', () => { + for (const text of ['', 'tomorrow', '2m', 'every', 'in two minutes']) { + expect(() => parseSessionWakeupSchedule(text, options)).toThrow( + SessionWakeupValidationError, + ); + } + expect(() => parseSessionWakeupSchedule('2m', options)).toThrow( + /say "in \.\.\."/, + ); + expect(() => parseSessionWakeupSchedule('soon', options)).toThrow( + /"in m\|h\|d"/, + ); + expect(() => + parseSessionWakeupSchedule('every 10m until soon', options), + ).toThrow(/ISO 8601/); + expect(() => + parseSessionWakeupSchedule('every 10m banana', options), + ).toThrow(/unexpected "banana"/); + }); +}); diff --git a/packages/cloud-agents/src/server/session-wakeups/parse.ts b/packages/cloud-agents/src/server/session-wakeups/parse.ts new file mode 100644 index 000000000..0ac578aea --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/parse.ts @@ -0,0 +1,190 @@ +import { + SESSION_WAKEUP_MAX_RUNS_LIMIT, + SESSION_WAKEUP_SCHEDULE_GRAMMAR, + type SessionWakeupSchedule, +} from '@roomote/types'; + +import { + SessionWakeupValidationError, + normalizeSessionWakeupSchedule, + validateSessionWakeupCaps, +} from './schedule'; + +export type ParsedSessionWakeupSchedule = { + schedule: SessionWakeupSchedule; + firstRunAt: Date; + maxRuns: number | null; + until: Date | null; +}; + +const UNIT_MINUTES: Record = { + m: 1, + min: 1, + mins: 1, + minute: 1, + minutes: 1, + h: 60, + hr: 60, + hrs: 60, + hour: 60, + hours: 60, + d: 24 * 60, + day: 24 * 60, + days: 24 * 60, +}; + +// Every pattern below runs on text whose whitespace has already been +// collapsed to single spaces and whose length is capped by the contract, so +// the patterns use literal single spaces and stay linear. +const DURATION = String.raw`(\d+) ?(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)`; +const DURATION_RE = new RegExp(`^${DURATION}$`, 'i'); +const IN_RE = new RegExp(`^(?:once )?in ${DURATION}$`, 'i'); +const AT_RE = /^(?:once )?at (\S+)$/i; +const EVERY_RE = new RegExp( + `^every (?:${DURATION}|(minute|hour|day))(?: (.*))?$`, + 'i', +); +const CRON_RE = /^cron ((?:\S+ ){4}\S+)(?: (.*))?$/i; +// A timezone token is anything that is not a modifier keyword or number. +const MODIFIER_START_RE = /^(?:x ?\d*|\d+|for|until)$/i; +const COUNT_RE = + /^(?:x ?(\d+)|(\d+) ?(?:x|times|runs)|for (\d+) (?:runs|times))$/i; +const UNTIL_RE = /^until (\S+)$/i; + +function invalid(text: string, detail?: string): SessionWakeupValidationError { + return new SessionWakeupValidationError( + `Could not read the schedule "${text}"${detail ? `: ${detail}` : ''}. ${SESSION_WAKEUP_SCHEDULE_GRAMMAR}`, + ); +} + +function durationMinutes(amount: string, unit: string): number { + const minutes = + Number.parseInt(amount, 10) * UNIT_MINUTES[unit.toLowerCase()]!; + if (!Number.isFinite(minutes) || minutes <= 0) { + throw new SessionWakeupValidationError('A duration must be positive.'); + } + return minutes; +} + +/** + * Split the trailing modifiers of an "every" schedule: any of "x3", + * "3 times", "for 3 runs", and "until ", in either order. + */ +function parseModifiers( + text: string, + rest: string | undefined, +): { maxRuns: number | null; until: Date | null } { + let maxRuns: number | null = null; + let until: Date | null = null; + const tokens = (rest ?? '').trim().split(' ').filter(Boolean); + let index = 0; + while (index < tokens.length) { + const one = tokens[index]!; + const two = tokens.slice(index, index + 2).join(' '); + const three = tokens.slice(index, index + 3).join(' '); + const untilMatch = UNTIL_RE.exec(two); + if (untilMatch) { + const parsed = new Date(untilMatch[1]!); + if (Number.isNaN(parsed.getTime())) { + throw invalid(text, `"${untilMatch[1]}" is not an ISO 8601 date-time`); + } + until = parsed; + index += 2; + continue; + } + const countMatch = + COUNT_RE.exec(three) ?? COUNT_RE.exec(two) ?? COUNT_RE.exec(one); + if (countMatch) { + const count = Number.parseInt( + countMatch[1] ?? countMatch[2] ?? countMatch[3]!, + 10, + ); + if (!(count >= 1) || count > SESSION_WAKEUP_MAX_RUNS_LIMIT) { + throw invalid( + text, + `run count must be between 1 and ${SESSION_WAKEUP_MAX_RUNS_LIMIT}`, + ); + } + maxRuns = count; + index += COUNT_RE.exec(three) ? 3 : COUNT_RE.exec(two) ? 2 : 1; + continue; + } + throw invalid(text, `unexpected "${one}"`); + } + return { maxRuns, until }; +} + +/** + * Parse the single schedule string the tool accepts into the normalized + * stored schedule plus its caps, resolving relative delays against `now`. + */ +export function parseSessionWakeupSchedule( + input: string, + options: { now: Date; defaultTimeZone: string }, +): ParsedSessionWakeupSchedule { + const text = input.trim().replace(/\s+/g, ' '); + if (!text) throw invalid(input, 'it is empty'); + + const inMatch = IN_RE.exec(text); + if (inMatch) { + const inMinutes = durationMinutes(inMatch[1]!, inMatch[2]!); + const normalized = normalizeSessionWakeupSchedule( + { mode: 'once', inMinutes }, + options, + ); + return { ...normalized, maxRuns: null, until: null }; + } + + const atMatch = AT_RE.exec(text); + if (atMatch) { + const normalized = normalizeSessionWakeupSchedule( + { mode: 'once', at: atMatch[1]! }, + options, + ); + return { ...normalized, maxRuns: null, until: null }; + } + + const everyMatch = EVERY_RE.exec(text); + if (everyMatch) { + const everyMinutes = everyMatch[3] + ? UNIT_MINUTES[everyMatch[3].toLowerCase()]! + : durationMinutes(everyMatch[1]!, everyMatch[2]!); + const normalized = normalizeSessionWakeupSchedule( + { mode: 'interval', everyMinutes }, + options, + ); + const caps = parseModifiers(text, everyMatch[4]); + validateSessionWakeupCaps({ ...normalized, ...caps }); + return { ...normalized, ...caps }; + } + + const cronMatch = CRON_RE.exec(text); + if (cronMatch) { + const trailing = (cronMatch[2] ?? '').trim().split(' ').filter(Boolean); + const timezone = + trailing[0] && !MODIFIER_START_RE.test(trailing[0]) + ? trailing.shift() + : undefined; + const normalized = normalizeSessionWakeupSchedule( + { + mode: 'cron', + expression: cronMatch[1]!, + ...(timezone ? { timezone } : {}), + }, + options, + ); + const caps = parseModifiers(text, trailing.join(' ')); + validateSessionWakeupCaps({ ...normalized, ...caps }); + return { ...normalized, ...caps }; + } + + // A bare duration ("2m", "10 minutes") is ambiguous between a delay and a + // cadence; say so rather than guess. + if (DURATION_RE.test(text)) { + throw invalid( + text, + 'say "in ..." for a one-shot delay or "every ..." for a repeating interval', + ); + } + throw invalid(text); +} diff --git a/packages/cloud-agents/src/server/session-wakeups/queue.ts b/packages/cloud-agents/src/server/session-wakeups/queue.ts new file mode 100644 index 000000000..226eaa581 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/queue.ts @@ -0,0 +1,67 @@ +import { Queue } from 'bullmq'; + +import { getRedis } from '@roomote/redis'; + +/** + * Delayed BullMQ jobs are only wakeup hints for `session_wakeups` rows; the + * row's `next_run_at` is the source of truth and the firing path claims it + * with a compare-and-set. The job id carries the occurrence time so the + * creator's hint and every recovery sweep collapse into one job, while a + * later occurrence of the same wakeup gets its own. + */ +export const SESSION_WAKEUP_QUEUE_NAME = 'session-wakeups'; +export const SESSION_WAKEUP_FIRE_JOB_NAME = 'fire'; + +export type SessionWakeupFireJob = { + wakeupId: string; + /** Unix milliseconds of the occurrence this job fires. */ + runAt: number; +}; + +let sessionWakeupQueue: Queue | null = null; + +function getSessionWakeupQueue(): Queue { + sessionWakeupQueue ??= new Queue( + SESSION_WAKEUP_QUEUE_NAME, + { + connection: getRedis(), + defaultJobOptions: { + attempts: 3, + backoff: { type: 'exponential', delay: 2_000 }, + removeOnComplete: true, + // PostgreSQL remains the source of truth; the recovery sweep re-adds + // a hint for any due row whose job was lost. + removeOnFail: true, + }, + }, + ); + return sessionWakeupQueue; +} + +export function buildSessionWakeupFireJobId(job: SessionWakeupFireJob): string { + // BullMQ rejects custom job ids containing ":". + return `${job.wakeupId}-${job.runAt}`; +} + +/** + * Add the delayed hint for one occurrence. Failure is not fatal for callers + * that already persisted the row: the recovery sweep re-adds it. + */ +export async function enqueueSessionWakeupFire( + job: SessionWakeupFireJob, +): Promise { + await getSessionWakeupQueue().add(SESSION_WAKEUP_FIRE_JOB_NAME, job, { + jobId: buildSessionWakeupFireJobId(job), + delay: Math.max(0, job.runAt - Date.now()), + }); +} + +export function enqueueSessionWakeupFireBestEffort( + job: SessionWakeupFireJob, +): void { + void enqueueSessionWakeupFire(job).catch((error) => { + console.error( + `[SessionWakeups] Persisted wakeup ${job.wakeupId}, but its delayed job failed to enqueue: ${error instanceof Error ? error.message : String(error)}`, + ); + }); +} diff --git a/packages/cloud-agents/src/server/session-wakeups/schedule.test.ts b/packages/cloud-agents/src/server/session-wakeups/schedule.test.ts new file mode 100644 index 000000000..4dd45aadf --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/schedule.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, it } from 'vitest'; + +import { + SessionWakeupValidationError, + computeNextSessionWakeupRunAt, + describeSessionWakeupSchedule, + normalizeSessionWakeupSchedule, + resolveSessionWakeupNextRun, + validateSessionWakeupCaps, +} from './schedule'; + +const now = new Date('2026-09-04T17:00:00.000Z'); +const options = { now, defaultTimeZone: 'America/New_York' }; + +describe('normalizeSessionWakeupSchedule', () => { + it('resolves a relative once schedule against now', () => { + const result = normalizeSessionWakeupSchedule( + { mode: 'once', inMinutes: 20 }, + options, + ); + expect(result.firstRunAt.toISOString()).toBe('2026-09-04T17:20:00.000Z'); + expect(result.schedule).toEqual({ + mode: 'once', + at: '2026-09-04T17:20:00.000Z', + inMinutes: 20, + }); + }); + + it('accepts an absolute once schedule with an offset', () => { + const result = normalizeSessionWakeupSchedule( + { mode: 'once', at: '2026-09-04T15:00:00-04:00' }, + options, + ); + expect(result.firstRunAt.toISOString()).toBe('2026-09-04T19:00:00.000Z'); + }); + + it('prefers inMinutes when a computed at is sent alongside it', () => { + expect( + normalizeSessionWakeupSchedule( + { mode: 'once', inMinutes: 5, at: '2026-09-04T18:00:00Z' }, + options, + ).firstRunAt.toISOString(), + ).toBe('2026-09-04T17:05:00.000Z'); + }); + + it('rejects a once schedule with neither time field', () => { + expect(() => + normalizeSessionWakeupSchedule({ mode: 'once' }, options), + ).toThrow(SessionWakeupValidationError); + }); + + it('rejects a once schedule in the past and one beyond the horizon', () => { + expect(() => + normalizeSessionWakeupSchedule( + { mode: 'once', at: '2026-09-04T16:59:00Z' }, + options, + ), + ).toThrow(/must be in the future/); + expect(() => + normalizeSessionWakeupSchedule( + { mode: 'once', at: '2026-11-04T17:00:00Z' }, + options, + ), + ).toThrow(/30 days/); + }); + + it('schedules the first interval run one interval from now', () => { + const result = normalizeSessionWakeupSchedule( + { mode: 'interval', everyMinutes: 15 }, + options, + ); + expect(result.firstRunAt.toISOString()).toBe('2026-09-04T17:15:00.000Z'); + }); + + it('resolves cron in the deployment timezone by default', () => { + const result = normalizeSessionWakeupSchedule( + { mode: 'cron', expression: '0 9 * * 1-5' }, + options, + ); + expect(result.schedule).toEqual({ + mode: 'cron', + expression: '0 9 * * 1-5', + timezone: 'America/New_York', + }); + // 2026-09-04 is a Friday; 17:00Z is 13:00 in New York, so the next + // weekday 9am is Monday 2026-09-07 09:00 EDT. + expect(result.firstRunAt.toISOString()).toBe('2026-09-07T13:00:00.000Z'); + }); + + it('rejects malformed cron and unknown timezones', () => { + expect(() => + normalizeSessionWakeupSchedule( + { mode: 'cron', expression: '0 9 * *' }, + options, + ), + ).toThrow(/five-field/); + expect(() => + normalizeSessionWakeupSchedule( + { mode: 'cron', expression: '0 9 * * *', timezone: 'Mars/Olympus' }, + options, + ), + ).toThrow(/timezone/); + }); +}); + +describe('resolveSessionWakeupNextRun', () => { + it('ends a once schedule after it fires', () => { + expect( + resolveSessionWakeupNextRun({ + schedule: { mode: 'once', at: now.toISOString() }, + firedAt: now, + runCountAfterFire: 1, + maxRuns: null, + until: null, + }), + ).toBeNull(); + }); + + it('computes the next interval from the fire time, not the missed slot', () => { + const firedAt = new Date('2026-09-04T20:03:00.000Z'); + expect( + resolveSessionWakeupNextRun({ + schedule: { mode: 'interval', everyMinutes: 10 }, + firedAt, + runCountAfterFire: 4, + maxRuns: null, + until: null, + })?.toISOString(), + ).toBe('2026-09-04T20:13:00.000Z'); + }); + + it('honours maxRuns and until', () => { + const schedule = { mode: 'interval' as const, everyMinutes: 10 }; + expect( + resolveSessionWakeupNextRun({ + schedule, + firedAt: now, + runCountAfterFire: 3, + maxRuns: 3, + until: null, + }), + ).toBeNull(); + expect( + resolveSessionWakeupNextRun({ + schedule, + firedAt: now, + runCountAfterFire: 1, + maxRuns: null, + until: new Date('2026-09-04T17:05:00.000Z'), + }), + ).toBeNull(); + expect( + resolveSessionWakeupNextRun({ + schedule, + firedAt: now, + runCountAfterFire: 1, + maxRuns: null, + until: new Date('2026-09-04T18:00:00.000Z'), + })?.toISOString(), + ).toBe('2026-09-04T17:10:00.000Z'); + }); +}); + +describe('validateSessionWakeupCaps', () => { + it('ignores stray caps on a once schedule', () => { + expect(() => + validateSessionWakeupCaps({ + schedule: { mode: 'once', at: now.toISOString() }, + firstRunAt: now, + maxRuns: 2, + until: null, + }), + ).not.toThrow(); + }); + + it('requires a cap on tight intervals', () => { + expect(() => + validateSessionWakeupCaps({ + schedule: { mode: 'interval', everyMinutes: 2 }, + firstRunAt: now, + maxRuns: null, + until: null, + }), + ).toThrow(/run count|end time/); + expect(() => + validateSessionWakeupCaps({ + schedule: { mode: 'interval', everyMinutes: 2 }, + firstRunAt: now, + maxRuns: 10, + until: null, + }), + ).not.toThrow(); + }); + + it('requires until to follow the first occurrence', () => { + expect(() => + validateSessionWakeupCaps({ + schedule: { mode: 'interval', everyMinutes: 30 }, + firstRunAt: new Date('2026-09-04T17:30:00.000Z'), + maxRuns: null, + until: new Date('2026-09-04T17:10:00.000Z'), + }), + ).toThrow(/later than the first occurrence/); + }); +}); + +describe('describeSessionWakeupSchedule', () => { + it('renders each mode for humans', () => { + expect( + describeSessionWakeupSchedule({ mode: 'interval', everyMinutes: 90 }), + ).toBe('every 90 minutes'); + expect( + describeSessionWakeupSchedule({ mode: 'interval', everyMinutes: 120 }), + ).toBe('every 2 hours'); + expect( + describeSessionWakeupSchedule({ mode: 'interval', everyMinutes: 1440 }), + ).toBe('every 1 day'); + expect( + describeSessionWakeupSchedule({ + mode: 'cron', + expression: '0 9 * * 1-5', + timezone: 'UTC', + }), + ).toBe('cron 0 9 * * 1-5 (UTC)'); + }); + + it('computes the next cron occurrence after a given time', () => { + expect( + computeNextSessionWakeupRunAt( + { mode: 'cron', expression: '*/15 * * * *', timezone: 'UTC' }, + new Date('2026-09-04T17:01:00.000Z'), + )?.toISOString(), + ).toBe('2026-09-04T17:15:00.000Z'); + }); +}); diff --git a/packages/cloud-agents/src/server/session-wakeups/schedule.ts b/packages/cloud-agents/src/server/session-wakeups/schedule.ts new file mode 100644 index 000000000..d5336fbc0 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/schedule.ts @@ -0,0 +1,299 @@ +import { CronExpressionParser } from 'cron-parser'; + +import { + SESSION_WAKEUP_MAX_INTERVAL_MINUTES, + SESSION_WAKEUP_MAX_ONCE_HORIZON_MINUTES, + SESSION_WAKEUP_MIN_INTERVAL_MINUTES, + SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES, + type SessionWakeupSchedule, +} from '@roomote/types'; + +/** The structured form the schedule-string parser produces before validation. */ +export type SessionWakeupScheduleInput = + | { mode: 'once'; inMinutes?: number; at?: string } + | { mode: 'interval'; everyMinutes: number } + | { mode: 'cron'; expression: string; timezone?: string }; + +const MINUTE_MS = 60_000; + +/** A schedule or option the agent supplied that cannot be honoured. */ +export class SessionWakeupValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'SessionWakeupValidationError'; + } +} + +export function normalizeSessionWakeupTimeZone(value: string): string { + const trimmed = value.trim(); + if (!trimmed) { + throw new SessionWakeupValidationError('Timezone is required.'); + } + try { + return new Intl.DateTimeFormat('en-US', { + timeZone: trimmed, + }).resolvedOptions().timeZone; + } catch { + throw new SessionWakeupValidationError( + `"${trimmed}" is not a valid IANA timezone.`, + ); + } +} + +function parseIsoDate(value: string, field: string): Date { + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + throw new SessionWakeupValidationError( + `${field} must be an ISO 8601 date-time.`, + ); + } + return parsed; +} + +function nextCronOccurrence( + expression: string, + timeZone: string, + from: Date, +): Date { + const interval = CronExpressionParser.parse(expression, { + currentDate: from, + tz: timeZone, + }); + return interval.next().toDate(); +} + +export type NormalizedSessionWakeupSchedule = { + schedule: SessionWakeupSchedule; + firstRunAt: Date; +}; + +/** + * Validate the agent-supplied schedule and resolve its first occurrence. + * Relative delays are resolved against `now`; cron expressions default to + * the deployment timezone. + */ +export function normalizeSessionWakeupSchedule( + input: SessionWakeupScheduleInput, + options: { now: Date; defaultTimeZone: string }, +): NormalizedSessionWakeupSchedule { + const { now } = options; + switch (input.mode) { + case 'once': { + // Models often send a computed `at` alongside `inMinutes`; the relative + // form is authoritative because it cannot be off by a clock skew. + const hasDelay = input.inMinutes !== undefined; + const hasAt = input.at !== undefined; + if (!hasDelay && !hasAt) { + throw new SessionWakeupValidationError( + 'A once schedule needs inMinutes or at.', + ); + } + const at = hasDelay + ? new Date(now.getTime() + input.inMinutes! * MINUTE_MS) + : parseIsoDate(input.at!, 'at'); + if (at.getTime() <= now.getTime()) { + throw new SessionWakeupValidationError( + `at must be in the future. The current time is ${now.toISOString()}.`, + ); + } + if ( + at.getTime() - now.getTime() > + SESSION_WAKEUP_MAX_ONCE_HORIZON_MINUTES * MINUTE_MS + ) { + throw new SessionWakeupValidationError( + 'A once schedule may be at most 30 days out.', + ); + } + return { + schedule: { + mode: 'once', + at: at.toISOString(), + ...(hasDelay ? { inMinutes: input.inMinutes } : {}), + }, + firstRunAt: at, + }; + } + case 'interval': { + if ( + !Number.isInteger(input.everyMinutes) || + input.everyMinutes < SESSION_WAKEUP_MIN_INTERVAL_MINUTES || + input.everyMinutes > SESSION_WAKEUP_MAX_INTERVAL_MINUTES + ) { + throw new SessionWakeupValidationError( + `everyMinutes must be a whole number between ${SESSION_WAKEUP_MIN_INTERVAL_MINUTES} and ${SESSION_WAKEUP_MAX_INTERVAL_MINUTES}.`, + ); + } + return { + schedule: { mode: 'interval', everyMinutes: input.everyMinutes }, + firstRunAt: new Date(now.getTime() + input.everyMinutes * MINUTE_MS), + }; + } + case 'cron': { + const timezone = normalizeSessionWakeupTimeZone( + input.timezone ?? options.defaultTimeZone, + ); + const expression = input.expression.trim().replace(/\s+/g, ' '); + if (expression.split(' ').length !== 5) { + throw new SessionWakeupValidationError( + 'Use a standard five-field cron expression.', + ); + } + let firstRunAt: Date; + try { + firstRunAt = nextCronOccurrence(expression, timezone, now); + } catch (error) { + throw new SessionWakeupValidationError( + `Invalid cron expression "${expression}": ${error instanceof Error ? error.message : String(error)}`, + ); + } + return { + schedule: { mode: 'cron', expression, timezone }, + firstRunAt, + }; + } + } +} + +/** + * The occurrence after `from` for a stored schedule, ignoring run caps. A + * once schedule has no occurrence after it fires. + */ +export function computeNextSessionWakeupRunAt( + schedule: SessionWakeupSchedule, + from: Date, +): Date | null { + switch (schedule.mode) { + case 'once': { + const at = new Date(schedule.at); + return at.getTime() > from.getTime() ? at : null; + } + case 'interval': + return new Date(from.getTime() + schedule.everyMinutes * MINUTE_MS); + case 'cron': + return nextCronOccurrence(schedule.expression, schedule.timezone, from); + } +} + +/** + * The occurrence to schedule after a fire, or null when the wakeup is done. + * Missed occurrences are computed from `firedAt`, not from the missed slot, + * so a deployment that was down for hours fires a monitor once on recovery + * rather than once per missed slot. + */ +export function resolveSessionWakeupNextRun(params: { + schedule: SessionWakeupSchedule; + firedAt: Date; + runCountAfterFire: number; + maxRuns: number | null; + until: Date | null; +}): Date | null { + if (params.schedule.mode === 'once') return null; + if (params.maxRuns !== null && params.runCountAfterFire >= params.maxRuns) { + return null; + } + const next = computeNextSessionWakeupRunAt(params.schedule, params.firedAt); + if (!next) return null; + if (params.until && next.getTime() > params.until.getTime()) return null; + return next; +} + +/** + * Enforce the run-cap rules for a normalized schedule: caps only apply to + * recurring schedules, tight intervals must be capped, and `until` must lie + * ahead of the first occurrence. + */ +export function validateSessionWakeupCaps(params: { + schedule: SessionWakeupSchedule; + firstRunAt: Date; + maxRuns: number | null; + until: Date | null; +}): void { + const { schedule } = params; + const uncapped = params.maxRuns === null && params.until === null; + // A once schedule is inherently a single run; a stray maxRuns or until from + // the model is ignored rather than rejected. + if (schedule.mode === 'once') return; + if (params.until && params.until.getTime() <= params.firstRunAt.getTime()) { + throw new SessionWakeupValidationError( + `until must be later than the first occurrence at ${params.firstRunAt.toISOString()}.`, + ); + } + if ( + schedule.mode === 'interval' && + schedule.everyMinutes < SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES && + uncapped + ) { + throw new SessionWakeupValidationError( + `Intervals under ${SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES} minutes need a run count ("x") or an end time ("until ") so they cannot run forever.`, + ); + } + // A cron expression can fire as often as every minute; hold it to the same + // bound as an interval by sampling the gap between upcoming occurrences. + if ( + schedule.mode === 'cron' && + uncapped && + estimateCronMinGapMinutes(schedule, params.firstRunAt) < + SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES + ) { + throw new SessionWakeupValidationError( + `Cron schedules that fire more often than every ${SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES} minutes need a run count ("x") or an end time ("until ") so they cannot run forever.`, + ); + } +} + +const CRON_GAP_SAMPLES = 24; + +/** + * The smallest gap, in minutes, between the next `CRON_GAP_SAMPLES` + * occurrences after `from`. A sample is enough to catch every-minute and + * every-few-minutes patterns, which are the ones the cap exists for. + */ +function estimateCronMinGapMinutes( + schedule: Extract, + from: Date, +): number { + const interval = CronExpressionParser.parse(schedule.expression, { + currentDate: from, + tz: schedule.timezone, + }); + let previous: number | null = null; + let minGap = Number.POSITIVE_INFINITY; + for (let index = 0; index < CRON_GAP_SAMPLES; index += 1) { + let next: number; + try { + next = interval.next().getTime(); + } catch { + break; + } + if (previous !== null) { + minGap = Math.min(minGap, (next - previous) / MINUTE_MS); + } + previous = next; + } + return minGap; +} + +function formatMinutes(minutes: number): string { + if (minutes % (24 * 60) === 0) { + const days = minutes / (24 * 60); + return `${days} day${days === 1 ? '' : 's'}`; + } + if (minutes % 60 === 0) { + const hours = minutes / 60; + return `${hours} hour${hours === 1 ? '' : 's'}`; + } + return `${minutes} minute${minutes === 1 ? '' : 's'}`; +} + +export function describeSessionWakeupSchedule( + schedule: SessionWakeupSchedule, +): string { + switch (schedule.mode) { + case 'once': + return `once at ${schedule.at}`; + case 'interval': + return `every ${formatMinutes(schedule.everyMinutes)}`; + case 'cron': + return `cron ${schedule.expression} (${schedule.timezone})`; + } +} diff --git a/packages/cloud-agents/src/server/session-wakeups/service.ts b/packages/cloud-agents/src/server/session-wakeups/service.ts new file mode 100644 index 000000000..43bfef638 --- /dev/null +++ b/packages/cloud-agents/src/server/session-wakeups/service.ts @@ -0,0 +1,290 @@ +import { + admitSessionWakeup, + cancelSessionWakeup, + db, + deploymentSettings, + eq, + getSessionWakeupById, + listSessionWakeups, + type SessionWakeup, +} from '@roomote/db/server'; +import { + MAX_ACTIVE_SESSION_WAKEUPS, + isSessionWakeupRecurring, + type ManageWakeupsInput, + type SessionWakeupReportPolicy, + type SessionWakeupSummary, +} from '@roomote/types'; + +import { enqueueSessionWakeupFireBestEffort } from './queue'; +import { parseSessionWakeupSchedule } from './parse'; +import { + SessionWakeupValidationError, + describeSessionWakeupSchedule, + normalizeSessionWakeupTimeZone, +} from './schedule'; + +const DEFAULT_DEPLOYMENT_SETTINGS_ID = 'default'; + +/** The conversation a wakeup tool call acts on, and who is acting. */ +export type SessionWakeupActor = { + conversationId: string; + userId: string; +}; + +export type CreateSessionWakeupInput = { + name: string; + prompt: string; + /** One schedule string, e.g. "in 20m", "every 10m x3", "cron 0 9 * * 1-5". */ + schedule: string; + reportPolicy?: SessionWakeupReportPolicy | null; +}; + +export type CreateSessionWakeupResult = { + wakeup: SessionWakeupSummary; + /** True when an equivalent active wakeup already existed and was reused. */ + duplicate: boolean; + timeZone: string; +}; + +/** + * Cron defaults and next-run confirmations use the deployment timezone when + * one is configured, otherwise UTC. The Slack-workspace fallback that + * custom automations use lives in the SDK and is not needed here: the agent + * can always name a timezone explicitly. + */ +export async function resolveSessionWakeupTimeZone(): Promise { + const settings = await db.query.deploymentSettings.findFirst({ + where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_SETTINGS_ID), + columns: { timeZone: true }, + }); + if (!settings?.timeZone) return 'UTC'; + try { + return normalizeSessionWakeupTimeZone(settings.timeZone); + } catch { + return 'UTC'; + } +} + +export function toSessionWakeupSummary( + row: SessionWakeup, +): SessionWakeupSummary { + return { + id: row.id, + name: row.name, + prompt: row.prompt, + schedule: row.schedule, + scheduleDescription: describeSessionWakeupSchedule(row.schedule), + reportPolicy: row.reportPolicy, + status: row.status, + runCount: row.runCount, + maxRuns: row.maxRuns, + until: row.until?.toISOString() ?? null, + nextRunAt: row.nextRunAt?.toISOString() ?? null, + lastFiredAt: row.lastFiredAt?.toISOString() ?? null, + lastError: row.lastError, + createdAt: row.createdAt.toISOString(), + }; +} + +export async function createSessionWakeup( + actor: SessionWakeupActor, + input: CreateSessionWakeupInput, + options: { now?: Date } = {}, +): Promise { + const now = options.now ?? new Date(); + const name = input.name.trim().replace(/\s+/g, ' '); + const prompt = input.prompt.trim(); + if (!name) throw new SessionWakeupValidationError('name is required.'); + if (!prompt) throw new SessionWakeupValidationError('prompt is required.'); + + const timeZone = await resolveSessionWakeupTimeZone(); + const { schedule, firstRunAt, maxRuns, until } = parseSessionWakeupSchedule( + input.schedule, + { now, defaultTimeZone: timeZone }, + ); + const recurring = isSessionWakeupRecurring(schedule); + const reportPolicy: SessionWakeupReportPolicy = + input.reportPolicy ?? (recurring ? 'only_when_notable' : 'always'); + + const result = await admitSessionWakeup({ + conversationId: actor.conversationId, + createdByUserId: actor.userId, + name, + prompt, + schedule, + reportPolicy, + maxRuns, + until, + nextRunAt: firstRunAt, + }); + if (result.outcome === 'cap_reached') { + throw new SessionWakeupValidationError( + `This conversation already has ${MAX_ACTIVE_SESSION_WAKEUPS} active wakeups. Cancel one before creating another.`, + ); + } + if (result.outcome === 'created') { + enqueueSessionWakeupFireBestEffort({ + wakeupId: result.wakeup.id, + runAt: firstRunAt.getTime(), + }); + } + + return { + wakeup: toSessionWakeupSummary(result.wakeup), + duplicate: result.outcome === 'duplicate', + timeZone, + }; +} + +export async function listSessionWakeupsForConversation( + conversationId: string, + options: { includeTerminal?: boolean } = {}, +): Promise { + const rows = await listSessionWakeups(conversationId, options); + return rows.map(toSessionWakeupSummary); +} + +export async function getSessionWakeupForConversation( + conversationId: string, + wakeupId: string, +): Promise { + const row = await getSessionWakeupById(wakeupId); + if (!row || row.conversationId !== conversationId) return null; + return toSessionWakeupSummary(row); +} + +export type CancelSessionWakeupResult = + | { outcome: 'cancelled'; wakeup: SessionWakeupSummary } + | { outcome: 'already_terminal'; wakeup: SessionWakeupSummary } + | { outcome: 'not_found' }; + +export async function cancelSessionWakeupForConversation( + conversationId: string, + wakeupId: string, +): Promise { + const cancelled = await cancelSessionWakeup({ id: wakeupId, conversationId }); + if (cancelled) { + return { outcome: 'cancelled', wakeup: toSessionWakeupSummary(cancelled) }; + } + const row = await getSessionWakeupById(wakeupId); + if (!row || row.conversationId !== conversationId) { + return { outcome: 'not_found' }; + } + return { outcome: 'already_terminal', wakeup: toSessionWakeupSummary(row) }; +} + +function formatNextRun(nextRunAt: string | null, timeZone: string): string { + if (!nextRunAt) return 'no further runs'; + const date = new Date(nextRunAt); + const local = new Intl.DateTimeFormat('en-US', { + timeZone, + dateStyle: 'medium', + timeStyle: 'short', + }).format(date); + return `${local} ${timeZone} (${date.toISOString()})`; +} + +/** + * Execute one `manage_wakeups` call on behalf of a Fast turn. Every branch + * returns a JSON-serializable result the model can read; validation + * problems come back as `{ success: false, error }` rather than throwing so + * the turn can correct and retry. + */ +export async function handleManageWakeupsToolCall( + actor: SessionWakeupActor, + input: ManageWakeupsInput, +): Promise> { + try { + switch (input.action) { + case 'create': { + if (!input.name || !input.prompt || !input.schedule) { + return { + success: false, + error: 'create requires name, prompt, and schedule.', + }; + } + const result = await createSessionWakeup(actor, { + name: input.name, + prompt: input.prompt, + schedule: input.schedule, + reportPolicy: input.reportPolicy ?? null, + }); + return { + success: true, + duplicate: result.duplicate, + wakeup: result.wakeup, + timeZone: result.timeZone, + nextRunLocal: formatNextRun(result.wakeup.nextRunAt, result.timeZone), + note: result.duplicate + ? 'An equivalent wakeup was already active in this conversation; it was reused instead of creating a duplicate.' + : 'Scheduled. When it fires you will receive a scheduled_wakeup platform event in this conversation. Confirm the plan to the user in one sentence.', + }; + } + case 'list': { + const timeZone = await resolveSessionWakeupTimeZone(); + const wakeups = await listSessionWakeupsForConversation( + actor.conversationId, + ); + return { + success: true, + now: new Date().toISOString(), + timeZone, + count: wakeups.length, + wakeups, + }; + } + case 'get': { + if (!input.wakeupId) { + return { success: false, error: 'wakeupId is required for get.' }; + } + const wakeup = await getSessionWakeupForConversation( + actor.conversationId, + input.wakeupId, + ); + if (!wakeup) { + return { + success: false, + error: 'No wakeup with that id exists in this conversation.', + }; + } + return { success: true, wakeup }; + } + case 'cancel': { + if (!input.wakeupId) { + return { success: false, error: 'wakeupId is required for cancel.' }; + } + const result = await cancelSessionWakeupForConversation( + actor.conversationId, + input.wakeupId, + ); + switch (result.outcome) { + case 'cancelled': + return { + success: true, + cancelled: true, + wakeup: result.wakeup, + note: `Cancelled "${result.wakeup.name}". It will not fire again.`, + }; + case 'already_terminal': + return { + success: true, + cancelled: false, + wakeup: result.wakeup, + note: `"${result.wakeup.name}" was already ${result.wakeup.status}.`, + }; + case 'not_found': + return { + success: false, + error: 'No wakeup with that id exists in this conversation.', + }; + } + } + } + } catch (error) { + if (error instanceof SessionWakeupValidationError) { + return { success: false, error: error.message }; + } + throw error; + } +} diff --git a/packages/db/drizzle/0077_normal_lionheart.sql b/packages/db/drizzle/0077_normal_lionheart.sql new file mode 100644 index 000000000..871d5fe93 --- /dev/null +++ b/packages/db/drizzle/0077_normal_lionheart.sql @@ -0,0 +1,28 @@ +CREATE TABLE "session_wakeups" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "conversation_id" uuid NOT NULL, + "created_by_user_id" text, + "name" text NOT NULL, + "prompt" text NOT NULL, + "prompt_signature" text NOT NULL, + "schedule" jsonb NOT NULL, + "report_policy" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "run_count" integer DEFAULT 0 NOT NULL, + "max_runs" integer, + "until" timestamp, + "consecutive_failures" integer DEFAULT 0 NOT NULL, + "next_run_at" timestamp, + "last_fired_at" timestamp, + "last_error" text, + "completed_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "session_wakeups_status_check" CHECK ("session_wakeups"."status" in ('active', 'completed', 'cancelled', 'failed')), + CONSTRAINT "session_wakeups_report_policy_check" CHECK ("session_wakeups"."report_policy" in ('always', 'only_when_notable')) +); +--> statement-breakpoint +ALTER TABLE "session_wakeups" ADD CONSTRAINT "session_wakeups_conversation_id_fast_agent_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."fast_agent_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session_wakeups" ADD CONSTRAINT "session_wakeups_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "session_wakeups_due_idx" ON "session_wakeups" USING btree ("status","next_run_at");--> statement-breakpoint +CREATE INDEX "session_wakeups_conversation_idx" ON "session_wakeups" USING btree ("conversation_id","status"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0077_snapshot.json b/packages/db/drizzle/meta/0077_snapshot.json new file mode 100644 index 000000000..7a5fc11df --- /dev/null +++ b/packages/db/drizzle/meta/0077_snapshot.json @@ -0,0 +1,14408 @@ +{ + "id": "016783b4-5c4d-409b-b5b5-11e62a98b417", + "prevId": "8d6cf3e9-e29b-4d3f-b53c-e0c7a5f0c8b0", + "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 + }, + "reasoning_effort": { + "name": "reasoning_effort", + "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 + }, + "brain_enabled": { + "name": "brain_enabled", + "type": "boolean", + "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": false + }, + "owner_automation": { + "name": "owner_automation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "current_reply_service_url": { + "name": "current_reply_service_url", + "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_owner_automation_idx": { + "name": "fast_agent_conversations_owner_automation_idx", + "columns": [ + { + "expression": "owner_automation", + "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": { + "fast_agent_conversations_owner_shape_check": { + "name": "fast_agent_conversations_owner_shape_check", + "value": "(\n (\"fast_agent_conversations\".\"user_id\" is not null and \"fast_agent_conversations\".\"owner_automation\" is null)\n or\n (\"fast_agent_conversations\".\"user_id\" is null and \"fast_agent_conversations\".\"owner_automation\" is not null)\n )" + } + }, + "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_parent_events": { + "name": "fast_agent_parent_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 + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent": { + "name": "parent", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "retry_task_start_run_id": { + "name": "retry_task_start_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "discarded_at": { + "name": "discarded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "admission": { + "name": "admission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "inference_retries": { + "name": "inference_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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_parent_events_pending_idx": { + "name": "fast_agent_parent_events_pending_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "discarded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "fast_agent_parent_events_retry_run_idx": { + "name": "fast_agent_parent_events_retry_run_idx", + "columns": [ + { + "expression": "retry_task_start_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fast_agent_parent_events_conversation_id_fast_agent_conversations_id_fk": { + "name": "fast_agent_parent_events_conversation_id_fast_agent_conversations_id_fk", + "tableFrom": "fast_agent_parent_events", + "tableTo": "fast_agent_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fast_agent_parent_events_retry_task_start_run_id_task_runs_id_fk": { + "name": "fast_agent_parent_events_retry_task_start_run_id_task_runs_id_fk", + "tableFrom": "fast_agent_parent_events", + "tableTo": "task_runs", + "columnsFrom": ["retry_task_start_run_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "fast_agent_parent_events_event_key_unique": { + "name": "fast_agent_parent_events_event_key_unique", + "nullsNotDistinct": false, + "columns": ["event_key"] + } + }, + "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.fast_agent_provider_messages": { + "name": "fast_agent_provider_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 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_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": { + "fast_agent_provider_messages_route_unique": { + "name": "fast_agent_provider_messages_route_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "fast_agent_provider_messages_conversation_idx": { + "name": "fast_agent_provider_messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "fast_agent_provider_messages_thread_idx": { + "name": "fast_agent_provider_messages_thread_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fast_agent_provider_messages_conversation_id_fast_agent_conversations_id_fk": { + "name": "fast_agent_provider_messages_conversation_id_fast_agent_conversations_id_fk", + "tableFrom": "fast_agent_provider_messages", + "tableTo": "fast_agent_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "fast_agent_provider_messages_provider_v3_check": { + "name": "fast_agent_provider_messages_provider_v3_check", + "value": "\"fast_agent_provider_messages\".\"provider\" in ('discord', 'slack', 'teams', 'telegram')" + } + }, + "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.session_wakeups": { + "name": "session_wakeups", + "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 + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_signature": { + "name": "prompt_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "report_policy": { + "name": "report_policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "until": { + "name": "until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "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": { + "session_wakeups_due_idx": { + "name": "session_wakeups_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_wakeups_conversation_idx": { + "name": "session_wakeups_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_wakeups_conversation_id_fast_agent_conversations_id_fk": { + "name": "session_wakeups_conversation_id_fast_agent_conversations_id_fk", + "tableFrom": "session_wakeups", + "tableTo": "fast_agent_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_wakeups_created_by_user_id_users_id_fk": { + "name": "session_wakeups_created_by_user_id_users_id_fk", + "tableFrom": "session_wakeups", + "tableTo": "users", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "session_wakeups_status_check": { + "name": "session_wakeups_status_check", + "value": "\"session_wakeups\".\"status\" in ('active', 'completed', 'cancelled', 'failed')" + }, + "session_wakeups_report_policy_check": { + "name": "session_wakeups_report_policy_check", + "value": "\"session_wakeups\".\"report_policy\" in ('always', 'only_when_notable')" + } + }, + "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 + }, + "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 + }, + "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 + }, + "responding_until": { + "name": "responding_until", + "type": "timestamp", + "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 + }, + "message_ts": { + "name": "message_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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_session_id_idx": { + "name": "task_artifacts_session_id_idx", + "columns": [ + { + "expression": "session_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": {} + }, + "task_artifacts_session_id_path_version_unique": { + "name": "task_artifacts_session_id_path_version_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"task_artifacts\".\"session_id\" IS NOT NULL", + "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_session_id_sessions_id_fk": { + "name": "task_artifacts_session_id_sessions_id_fk", + "tableFrom": "task_artifacts", + "tableTo": "sessions", + "columnsFrom": ["session_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": { + "task_artifacts_owner_shape_check": { + "name": "task_artifacts_owner_shape_check", + "value": "(\"task_artifacts\".\"task_id\" IS NOT NULL) <> (\"task_artifacts\".\"session_id\" IS NOT NULL)" + } + }, + "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 AND \"task_runs\".\"canceled_at\" IS 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 3e8c640c5..df7564c5f 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -540,6 +540,13 @@ "when": 1788530600978, "tag": "0076_fearless_skullbuster", "breakpoints": true + }, + { + "idx": 77, + "version": "7", + "when": 1788560388583, + "tag": "0077_normal_lionheart", + "breakpoints": true } ] } diff --git a/packages/db/src/lib/__tests__/session-wakeups.test.ts b/packages/db/src/lib/__tests__/session-wakeups.test.ts new file mode 100644 index 000000000..6cd48a8fb --- /dev/null +++ b/packages/db/src/lib/__tests__/session-wakeups.test.ts @@ -0,0 +1,366 @@ +// Real-DB coverage for session wakeups. The compare-and-set claim on +// next_run_at is load-bearing: it is what keeps a duplicate delayed job, or +// two workers holding the same job, from firing one occurrence twice. + +import { + MAX_ACTIVE_SESSION_WAKEUPS, + SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES, +} from '@roomote/types'; + +import { + admitSessionWakeup, + cancelSessionWakeup, + cancelSessionWakeupsForConversation, + claimSessionWakeupFire, + countActiveSessionWakeups, + db, + fastAgentConversations, + getSessionWakeupById, + insertSessionWakeup, + listDueSessionWakeups, + listSessionWakeups, + recordSessionWakeupOutcome, + sessionWakeups, + userFactory, +} from '../../server'; + +async function makeConversation() { + const user = await userFactory.create(); + const [conversation] = await db + .insert(fastAgentConversations) + .values({ + userId: user.id, + surface: 'web', + workspaceId: user.id, + conversationId: `conversation-${crypto.randomUUID()}`, + }) + .returning(); + return { user, conversation: conversation! }; +} + +const firstRunAt = new Date('2026-09-04T17:10:00.000Z'); + +async function makeWakeup( + conversationId: string, + userId: string, + overrides: Partial[0]> = {}, +) { + return insertSessionWakeup({ + conversationId, + createdByUserId: userId, + name: 'Check PR #85', + prompt: 'Check whether PR #85 merged.', + schedule: { mode: 'interval', everyMinutes: 10 }, + reportPolicy: 'only_when_notable', + maxRuns: null, + until: null, + nextRunAt: firstRunAt, + ...overrides, + }); +} + +afterEach(async () => { + await db.delete(sessionWakeups); +}); + +describe('session wakeup helpers', () => { + it('deduplicates relative retries with changed resolved times, not distinct delays or prompts', async () => { + const { user, conversation } = await makeConversation(); + const input = { + conversationId: conversation.id, + createdByUserId: user.id, + name: 'Reminder', + prompt: 'Check the deploy.', + schedule: { + mode: 'once' as const, + at: firstRunAt.toISOString(), + inMinutes: 2, + }, + reportPolicy: 'always' as const, + maxRuns: null, + until: null, + nextRunAt: firstRunAt, + }; + const first = await admitSessionWakeup(input); + expect(first.outcome).toBe('created'); + const later = new Date(firstRunAt.getTime() + 5_000); + const retry = { + ...input, + prompt: ' CHECK THE DEPLOY. ', + schedule: { ...input.schedule, at: later.toISOString() }, + nextRunAt: later, + }; + expect(await admitSessionWakeup(retry)).toEqual({ + ...first, + outcome: 'duplicate', + }); + const [persisted] = await listSessionWakeups(conversation.id); + expect(persisted!.schedule).toEqual(input.schedule); + expect(persisted!.nextRunAt).toEqual(firstRunAt); + expect( + await admitSessionWakeup({ + ...retry, + schedule: { ...retry.schedule, inMinutes: 3 }, + }), + ).toMatchObject({ outcome: 'created' }); + expect( + await admitSessionWakeup({ ...retry, prompt: 'Check another deploy.' }), + ).toMatchObject({ outcome: 'created' }); + expect(await listSessionWakeups(conversation.id)).toHaveLength(3); + }); + + it('does not infer relative identity from legacy or absolute one-shots', async () => { + const { user, conversation } = await makeConversation(); + const legacy = await makeWakeup(conversation.id, user.id, { + schedule: { mode: 'once', at: firstRunAt.toISOString() }, + }); + const input = { ...legacy, nextRunAt: firstRunAt }; + expect(await admitSessionWakeup(input)).toMatchObject({ + outcome: 'duplicate', + wakeup: { id: legacy.id }, + }); + expect( + await admitSessionWakeup({ + ...input, + schedule: { mode: 'once', at: firstRunAt.toISOString(), inMinutes: 2 }, + }), + ).toMatchObject({ outcome: 'created' }); + const later = new Date(firstRunAt.getTime() + 5_000); + expect( + await admitSessionWakeup({ + ...input, + schedule: { mode: 'once', at: later.toISOString() }, + nextRunAt: later, + }), + ).toMatchObject({ outcome: 'created' }); + }); + + it.each([ + { mode: 'once' as const, at: firstRunAt.toISOString() }, + { mode: 'cron' as const, expression: '*/10 * * * *', timezone: 'UTC' }, + ])( + 'deduplicates $mode schedules after JSONB reorders keys', + async (schedule) => { + const { user, conversation } = await makeConversation(); + const input = { + conversationId: conversation.id, + createdByUserId: user.id, + name: 'Check PR', + prompt: 'check pr', + schedule, + reportPolicy: 'only_when_notable' as const, + maxRuns: null, + until: null, + nextRunAt: firstRunAt, + }; + expect(await admitSessionWakeup(input)).toMatchObject({ + outcome: 'created', + }); + const [persisted] = await listSessionWakeups(conversation.id); + expect(persisted!.schedule).toEqual(schedule); + expect(Object.keys(persisted!.schedule)).not.toEqual( + Object.keys(schedule), + ); + + expect(await admitSessionWakeup(input)).toMatchObject({ + outcome: 'duplicate', + wakeup: { id: persisted!.id }, + }); + expect(await listSessionWakeups(conversation.id)).toHaveLength(1); + }, + ); + + it('deduplicates concurrent recurring creates in one conversation', async () => { + const { user, conversation } = await makeConversation(); + const results = await Promise.all( + Array.from({ length: 12 }, (_, index) => + admitSessionWakeup({ + conversationId: conversation.id, + createdByUserId: user.id, + name: `Check ${index}`, + prompt: index % 2 ? ' CHECK PR ' : 'check pr', + schedule: { mode: 'interval', everyMinutes: 10 }, + reportPolicy: 'only_when_notable', + maxRuns: null, + until: null, + nextRunAt: new Date(firstRunAt.getTime() + index), + }), + ), + ); + + expect(results.filter((r) => r.outcome === 'created')).toHaveLength(1); + expect(results.filter((r) => r.outcome === 'duplicate')).toHaveLength(11); + const rows = await listSessionWakeups(conversation.id); + expect(rows).toHaveLength(1); + for (const result of results) { + expect(result).toMatchObject({ wakeup: { id: rows[0]!.id } }); + } + }); + + it('caps concurrent distinct creates at ten and still admits duplicates', async () => { + const { user, conversation } = await makeConversation(); + const input = { + conversationId: conversation.id, + createdByUserId: user.id, + name: 'Check PR', + prompt: 'check pr', + schedule: { mode: 'interval' as const, everyMinutes: 10 }, + reportPolicy: 'only_when_notable' as const, + maxRuns: null, + until: null, + nextRunAt: firstRunAt, + }; + const results = await Promise.all( + Array.from({ length: MAX_ACTIVE_SESSION_WAKEUPS + 5 }, (_, index) => + admitSessionWakeup({ ...input, prompt: `check pr ${index}` }), + ), + ); + + expect(results.filter((r) => r.outcome === 'created')).toHaveLength( + MAX_ACTIVE_SESSION_WAKEUPS, + ); + expect(results.filter((r) => r.outcome === 'cap_reached')).toHaveLength(5); + expect(await countActiveSessionWakeups(conversation.id)).toBe( + MAX_ACTIVE_SESSION_WAKEUPS, + ); + const rows = await listSessionWakeups(conversation.id); + expect( + await admitSessionWakeup({ ...input, prompt: rows[0]!.prompt }), + ).toMatchObject({ outcome: 'duplicate', wakeup: { id: rows[0]!.id } }); + }); + + it('claims an occurrence exactly once and advances the row', async () => { + const { user, conversation } = await makeConversation(); + const row = await makeWakeup(conversation.id, user.id); + const nextRunAt = new Date('2026-09-04T17:20:00.000Z'); + const firedAt = new Date('2026-09-04T17:10:02.000Z'); + + const claimed = await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: firstRunAt, + nextRunAt, + firedAt, + }); + expect(claimed?.runCount).toBe(1); + expect(claimed?.status).toBe('active'); + expect(claimed?.nextRunAt?.toISOString()).toBe(nextRunAt.toISOString()); + + // A duplicate job for the same occurrence finds the row already moved. + const duplicate = await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: firstRunAt, + nextRunAt: new Date('2026-09-04T17:30:00.000Z'), + firedAt, + }); + expect(duplicate).toBeNull(); + expect((await getSessionWakeupById(row.id))?.runCount).toBe(1); + }); + + it('completes the row when the claim carries no next occurrence', async () => { + const { user, conversation } = await makeConversation(); + const row = await makeWakeup(conversation.id, user.id, { + schedule: { mode: 'once', at: firstRunAt.toISOString() }, + reportPolicy: 'always', + }); + + const claimed = await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: firstRunAt, + nextRunAt: null, + firedAt: firstRunAt, + }); + expect(claimed?.status).toBe('completed'); + expect(claimed?.nextRunAt).toBeNull(); + expect(claimed?.completedAt).not.toBeNull(); + expect(await countActiveSessionWakeups(conversation.id)).toBe(0); + }); + + it('retires an active wakeup after enough consecutive failures', async () => { + const { user, conversation } = await makeConversation(); + const row = await makeWakeup(conversation.id, user.id); + + for (let i = 1; i < SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES; i += 1) { + const updated = await recordSessionWakeupOutcome({ + id: row.id, + status: 'failed', + error: `boom ${i}`, + }); + expect(updated?.status).toBe('active'); + expect(updated?.consecutiveFailures).toBe(i); + } + const succeeded = await recordSessionWakeupOutcome({ + id: row.id, + status: 'succeeded', + }); + expect(succeeded?.consecutiveFailures).toBe(0); + expect(succeeded?.lastError).toBeNull(); + + let last = null; + for (let i = 0; i < SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES; i += 1) { + last = await recordSessionWakeupOutcome({ + id: row.id, + status: 'failed', + error: 'still broken', + }); + } + expect(last?.status).toBe('failed'); + expect(last?.nextRunAt).toBeNull(); + expect(last?.lastError).toBe('still broken'); + }); + + it('cancels only active rows scoped to their conversation', async () => { + const { user, conversation } = await makeConversation(); + const other = await makeConversation(); + const row = await makeWakeup(conversation.id, user.id); + + expect( + await cancelSessionWakeup({ + id: row.id, + conversationId: other.conversation.id, + }), + ).toBeNull(); + const cancelled = await cancelSessionWakeup({ + id: row.id, + conversationId: conversation.id, + }); + expect(cancelled?.status).toBe('cancelled'); + expect(cancelled?.nextRunAt).toBeNull(); + expect( + await cancelSessionWakeup({ + id: row.id, + conversationId: conversation.id, + }), + ).toBeNull(); + }); + + it('cancels every active wakeup in a conversation and keeps history', async () => { + const { user, conversation } = await makeConversation(); + await makeWakeup(conversation.id, user.id, { name: 'A' }); + await makeWakeup(conversation.id, user.id, { + name: 'B', + prompt: 'Something else entirely.', + }); + + expect(await cancelSessionWakeupsForConversation(conversation.id)).toBe(2); + expect(await countActiveSessionWakeups(conversation.id)).toBe(0); + expect(await listSessionWakeups(conversation.id)).toHaveLength(0); + expect( + await listSessionWakeups(conversation.id, { includeTerminal: true }), + ).toHaveLength(2); + }); + + it('lists due rows for recovery', async () => { + const { user, conversation } = await makeConversation(); + const due = await makeWakeup(conversation.id, user.id, { name: 'Due' }); + await makeWakeup(conversation.id, user.id, { + name: 'Later', + prompt: 'Later prompt for a different check.', + nextRunAt: new Date('2026-09-04T18:00:00.000Z'), + }); + + const rows = await listDueSessionWakeups({ + dueBy: new Date('2026-09-04T17:12:00.000Z'), + }); + expect(rows.map((row) => row.id)).toEqual([due.id]); + }); +}); diff --git a/packages/db/src/lib/session-wakeups.ts b/packages/db/src/lib/session-wakeups.ts new file mode 100644 index 000000000..6fce84958 --- /dev/null +++ b/packages/db/src/lib/session-wakeups.ts @@ -0,0 +1,340 @@ +import { isDeepStrictEqual } from 'node:util'; + +import { and, asc, count, desc, eq, lte, sql } from 'drizzle-orm'; + +import { + MAX_ACTIVE_SESSION_WAKEUPS, + SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES, + type SessionWakeupReportPolicy, + type SessionWakeupSchedule, +} from '@roomote/types'; + +import { type DatabaseOrTransaction, db } from '../db'; +import { fastAgentConversations, sessionWakeups } from '../schema'; +import type { SessionWakeup } from '../types'; +import { runInTransactionIfAvailable } from './transaction-utils'; + +/** + * Collapse whitespace and case so two prompts that read the same dedupe to + * the same active wakeup. + */ +export function buildSessionWakeupPromptSignature(prompt: string): string { + return prompt.toLowerCase().replace(/\s+/g, ' ').trim(); +} + +export function countActiveSessionWakeups( + conversationId: string, + tx: DatabaseOrTransaction = db, +): Promise { + return tx + .select({ value: count() }) + .from(sessionWakeups) + .where( + and( + eq(sessionWakeups.conversationId, conversationId), + eq(sessionWakeups.status, 'active'), + ), + ) + .then((rows) => rows[0]?.value ?? 0); +} + +export function listActiveSessionWakeups( + conversationId: string, + tx: DatabaseOrTransaction = db, +): Promise { + return tx + .select() + .from(sessionWakeups) + .where( + and( + eq(sessionWakeups.conversationId, conversationId), + eq(sessionWakeups.status, 'active'), + ), + ) + .orderBy(asc(sessionWakeups.nextRunAt), asc(sessionWakeups.createdAt)); +} + +/** Active first, then the most recent terminal rows for history. */ +export function listSessionWakeups( + conversationId: string, + options: { includeTerminal?: boolean; limit?: number } = {}, + tx: DatabaseOrTransaction = db, +): Promise { + const limit = options.limit ?? 50; + return tx + .select() + .from(sessionWakeups) + .where( + options.includeTerminal + ? eq(sessionWakeups.conversationId, conversationId) + : and( + eq(sessionWakeups.conversationId, conversationId), + eq(sessionWakeups.status, 'active'), + ), + ) + .orderBy( + sql`case when ${sessionWakeups.status} = 'active' then 0 else 1 end`, + asc(sessionWakeups.nextRunAt), + desc(sessionWakeups.updatedAt), + ) + .limit(limit); +} + +export async function getSessionWakeupById( + id: string, + tx: DatabaseOrTransaction = db, +): Promise { + const [row] = await tx + .select() + .from(sessionWakeups) + .where(eq(sessionWakeups.id, id)) + .limit(1); + return row ?? null; +} + +export type InsertSessionWakeupInput = { + conversationId: string; + createdByUserId: string | null; + name: string; + prompt: string; + schedule: SessionWakeupSchedule; + reportPolicy: SessionWakeupReportPolicy; + maxRuns: number | null; + until: Date | null; + nextRunAt: Date; +}; + +export type AdmitSessionWakeupResult = + | { outcome: 'created' | 'duplicate'; wakeup: SessionWakeup } + | { outcome: 'cap_reached' }; + +/** Serialize create admission even when tools run concurrently within a turn. */ +export async function admitSessionWakeup( + input: InsertSessionWakeupInput, + database: DatabaseOrTransaction = db, +): Promise { + return runInTransactionIfAvailable(database, async (tx) => { + const [conversation] = await tx + .select({ id: fastAgentConversations.id }) + .from(fastAgentConversations) + .where(eq(fastAgentConversations.id, input.conversationId)) + .for('update'); + if (!conversation) { + throw new Error(`Conversation ${input.conversationId} does not exist.`); + } + + const promptSignature = buildSessionWakeupPromptSignature(input.prompt); + const active = await listActiveSessionWakeups(input.conversationId, tx); + const existing = active.find( + (row) => + row.promptSignature === promptSignature && + (row.schedule.mode === 'once' && + input.schedule.mode === 'once' && + row.schedule.inMinutes !== undefined && + input.schedule.inMinutes !== undefined + ? row.schedule.inMinutes === input.schedule.inMinutes + : isDeepStrictEqual(row.schedule, input.schedule)), + ); + if (existing) return { outcome: 'duplicate', wakeup: existing }; + if (active.length >= MAX_ACTIVE_SESSION_WAKEUPS) { + return { outcome: 'cap_reached' }; + } + + return { outcome: 'created', wakeup: await insertSessionWakeup(input, tx) }; + }); +} + +export async function insertSessionWakeup( + input: InsertSessionWakeupInput, + tx: DatabaseOrTransaction = db, +): Promise { + const [row] = await tx + .insert(sessionWakeups) + .values({ + conversationId: input.conversationId, + createdByUserId: input.createdByUserId, + name: input.name, + prompt: input.prompt, + promptSignature: buildSessionWakeupPromptSignature(input.prompt), + schedule: input.schedule, + reportPolicy: input.reportPolicy, + status: 'active', + maxRuns: input.maxRuns, + until: input.until, + nextRunAt: input.nextRunAt, + }) + .returning(); + if (!row) { + throw new Error('Failed to insert session wakeup.'); + } + return row; +} + +/** + * Cancel one active wakeup in a conversation. Returns the row when it was + * active and is now cancelled, and null when it was missing, belonged to + * another conversation, or had already reached a terminal state. + */ +export async function cancelSessionWakeup( + params: { id: string; conversationId: string }, + tx: DatabaseOrTransaction = db, +): Promise { + const now = new Date(); + const [row] = await tx + .update(sessionWakeups) + .set({ + status: 'cancelled', + nextRunAt: null, + completedAt: now, + updatedAt: now, + }) + .where( + and( + eq(sessionWakeups.id, params.id), + eq(sessionWakeups.conversationId, params.conversationId), + eq(sessionWakeups.status, 'active'), + ), + ) + .returning(); + return row ?? null; +} + +/** Cancel every active wakeup in a conversation, e.g. when it is archived. */ +export async function cancelSessionWakeupsForConversation( + conversationId: string, + tx: DatabaseOrTransaction = db, +): Promise { + const now = new Date(); + const rows = await tx + .update(sessionWakeups) + .set({ + status: 'cancelled', + nextRunAt: null, + completedAt: now, + updatedAt: now, + }) + .where( + and( + eq(sessionWakeups.conversationId, conversationId), + eq(sessionWakeups.status, 'active'), + ), + ) + .returning({ id: sessionWakeups.id }); + return rows.length; +} + +export type ClaimSessionWakeupFireInput = { + id: string; + /** The occurrence the caller intends to fire; the claim fails if it moved. */ + expectedNextRunAt: Date; + /** The occurrence after this one, or null when this is the last run. */ + nextRunAt: Date | null; + firedAt: Date; +}; + +/** + * Compare-and-set claim of one occurrence. Two workers holding the same + * delayed job cannot both fire it, and a stale job that arrives after the + * row already advanced is a no-op. The row completes when there is no next + * occurrence. + */ +export async function claimSessionWakeupFire( + input: ClaimSessionWakeupFireInput, + tx: DatabaseOrTransaction = db, +): Promise { + const [row] = await tx + .update(sessionWakeups) + .set({ + runCount: sql`${sessionWakeups.runCount} + 1`, + lastFiredAt: input.firedAt, + nextRunAt: input.nextRunAt, + ...(input.nextRunAt + ? {} + : { status: 'completed' as const, completedAt: input.firedAt }), + updatedAt: input.firedAt, + }) + .where( + and( + eq(sessionWakeups.id, input.id), + eq(sessionWakeups.status, 'active'), + eq(sessionWakeups.nextRunAt, input.expectedNextRunAt), + ), + ) + .returning(); + return row ?? null; +} + +/** + * Record how the turn a wakeup admitted ended. A success clears the failure + * streak; enough consecutive failures retire a still-active wakeup. + */ +export async function recordSessionWakeupOutcome( + params: { id: string; status: 'succeeded' | 'failed'; error?: string }, + tx: DatabaseOrTransaction = db, +): Promise { + const now = new Date(); + if (params.status === 'succeeded') { + const [row] = await tx + .update(sessionWakeups) + .set({ consecutiveFailures: 0, lastError: null, updatedAt: now }) + .where(eq(sessionWakeups.id, params.id)) + .returning(); + return row ?? null; + } + + const [row] = await tx + .update(sessionWakeups) + .set({ + consecutiveFailures: sql`${sessionWakeups.consecutiveFailures} + 1`, + lastError: params.error ?? null, + updatedAt: now, + }) + .where(eq(sessionWakeups.id, params.id)) + .returning(); + if (!row) return null; + if ( + row.status !== 'active' || + row.consecutiveFailures < SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES + ) { + return row; + } + + const [retired] = await tx + .update(sessionWakeups) + .set({ + status: 'failed', + nextRunAt: null, + completedAt: now, + updatedAt: now, + }) + .where( + and( + eq(sessionWakeups.id, params.id), + eq(sessionWakeups.status, 'active'), + ), + ) + .returning(); + return retired ?? row; +} + +/** + * Active wakeups whose occurrence is due on or before `dueBy`. The recovery + * sweep uses this to re-add lost delayed jobs; the claim keeps duplicates + * harmless. + */ +export function listDueSessionWakeups( + params: { dueBy: Date; limit?: number }, + tx: DatabaseOrTransaction = db, +): Promise[]> { + return tx + .select({ id: sessionWakeups.id, nextRunAt: sessionWakeups.nextRunAt }) + .from(sessionWakeups) + .where( + and( + eq(sessionWakeups.status, 'active'), + lte(sessionWakeups.nextRunAt, params.dueBy), + ), + ) + .orderBy(asc(sessionWakeups.nextRunAt)) + .limit(params.limit ?? 500); +} diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 13a266481..b341fbb06 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -83,6 +83,9 @@ import type { FastAgentSurface, ReasoningEffort, SessionStatus, + SessionWakeupReportPolicy, + SessionWakeupSchedule, + SessionWakeupStatus, } from '@roomote/types'; import { DEFAULT_TASK_ARTIFACT_TYPE } from '@roomote/types'; @@ -3363,6 +3366,76 @@ export const fastAgentConversationsRelations = relations( }), ); +/** + * session_wakeups + * + * Messages a Fast conversation scheduled for itself. A row stays `active` + * until it has fired for the last time, was cancelled, or failed too many + * turns in a row; terminal rows are retained for history. `next_run_at` is + * the source of truth for firing: the BullMQ delayed job is only a wakeup + * hint, and claiming an occurrence is a compare-and-set on that column. + */ +export const sessionWakeups = pgTable( + 'session_wakeups', + { + id: uuid('id').primaryKey().defaultRandom(), + conversationId: uuid('conversation_id') + .notNull() + .references(() => fastAgentConversations.id, { onDelete: 'cascade' }), + createdByUserId: text('created_by_user_id').references(() => users.id, { + onDelete: 'set null', + }), + name: text('name').notNull(), + prompt: text('prompt').notNull(), + /** Whitespace-collapsed, lower-cased prompt used to detect duplicates. */ + promptSignature: text('prompt_signature').notNull(), + schedule: jsonb('schedule').notNull().$type(), + reportPolicy: text('report_policy') + .notNull() + .$type(), + status: text('status') + .notNull() + .default('active') + .$type(), + runCount: integer('run_count').notNull().default(0), + maxRuns: integer('max_runs'), + until: timestamp('until'), + consecutiveFailures: integer('consecutive_failures').notNull().default(0), + nextRunAt: timestamp('next_run_at'), + lastFiredAt: timestamp('last_fired_at'), + lastError: text('last_error'), + completedAt: timestamp('completed_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => [ + index('session_wakeups_due_idx').on(table.status, table.nextRunAt), + index('session_wakeups_conversation_idx').on( + table.conversationId, + table.status, + ), + check( + 'session_wakeups_status_check', + sql`${table.status} in ('active', 'completed', 'cancelled', 'failed')`, + ), + check( + 'session_wakeups_report_policy_check', + sql`${table.reportPolicy} in ('always', 'only_when_notable')`, + ), + ], +); + +export const sessionWakeupsRelations = relations(sessionWakeups, ({ one }) => ({ + conversation: one(fastAgentConversations, { + fields: [sessionWakeups.conversationId], + references: [fastAgentConversations.id], + }), + createdByUser: one(users, { + fields: [sessionWakeups.createdByUserId], + references: [users.id], + }), +})); + export const fastAgentParentEventsRelations = relations( fastAgentParentEvents, ({ one }) => ({ diff --git a/packages/db/src/server.ts b/packages/db/src/server.ts index 55248d726..9849ff0ea 100644 --- a/packages/db/src/server.ts +++ b/packages/db/src/server.ts @@ -59,6 +59,7 @@ export * from './lib/sync-task-state'; export * from './lib/cancel-task-run'; export * from './lib/automations'; export * from './lib/custom-automations'; +export * from './lib/session-wakeups'; export * from './lib/background-automation-slack-threads'; export * from './lib/task-run-events'; export * from './lib/declarative-environments'; @@ -201,6 +202,8 @@ export { fastAgentMessagesRelations, fastAgentParentEvents, fastAgentParentEventsRelations, + sessionWakeups, + sessionWakeupsRelations, fastAgentProviderMessages, fastAgentProviderMessagesRelations, fastAgentPrFeedbackDeliveries, diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index 91546f2d4..f9ce2ec5f 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -64,6 +64,7 @@ import type { environmentRepositoryMappings, automations, customAutomations, + sessionWakeups, trackedMessages, } from './schema'; @@ -626,3 +627,14 @@ export type CreateCustomAutomation = Omit< typeof customAutomations.$inferInsert, Timestamp >; + +/** + * session_wakeups + */ + +export type SessionWakeup = typeof sessionWakeups.$inferSelect; + +export type CreateSessionWakeup = Omit< + typeof sessionWakeups.$inferInsert, + Timestamp +>; diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index 506c8b6de..c5faf9981 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -221,6 +221,15 @@ export { wakeFastAgentParentEventNow, type FastAgentParentEventQueueRequest, } from './lib/fast-agent-parent-event-queue'; +export { + SESSION_WAKEUP_FIRE_JOB_NAME, + SESSION_WAKEUP_QUEUE_NAME, + SESSION_WAKEUP_RECOVERY_LOOKAHEAD_MS, + fireSessionWakeup, + recoverPendingSessionWakeups, + type FireSessionWakeupResult, + type SessionWakeupFireJob, +} from './lib/session-wakeups'; export { admitFastAgentHumanFollowUp, persistFastAgentInlineHumanTurn, diff --git a/packages/sdk/src/server/lib/fast-agent-parent-event-queue.test.ts b/packages/sdk/src/server/lib/fast-agent-parent-event-queue.test.ts index 947c1bdcd..61c8e435d 100644 --- a/packages/sdk/src/server/lib/fast-agent-parent-event-queue.test.ts +++ b/packages/sdk/src/server/lib/fast-agent-parent-event-queue.test.ts @@ -33,6 +33,7 @@ const mocks = vi.hoisted(() => { deliver: vi.fn(), retryStartup: vi.fn(), recordAutomationOutcome: vi.fn(), + recordWakeupOutcome: vi.fn(), DeliveryError, }; }); @@ -87,6 +88,7 @@ vi.mock('@roomote/db/server', () => ({ lte: vi.fn((...values: unknown[]) => values), or: vi.fn((...values: unknown[]) => values), recordCustomAutomationRunOutcome: mocks.recordAutomationOutcome, + recordSessionWakeupOutcome: mocks.recordWakeupOutcome, sql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ strings: [...strings], values, @@ -347,6 +349,75 @@ describe('Fast parent event durable queue', () => { ); }); + it('records the outcome of a scheduled wakeup turn once delivery settles', async () => { + const wakeupEvent = { + type: 'scheduled_wakeup' as const, + eventId: 'wakeup-1:3', + wakeupId: 'wakeup-1', + name: 'Check PR #85', + prompt: 'Check whether PR #85 merged.', + runNumber: 3, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: '2026-09-04T17:20:00.000Z', + reportPolicy: 'only_when_notable' as const, + createdByUserId: 'user-1', + }; + const row = pendingRow('wakeup-event', wakeupEvent); + mocks.findPending + .mockResolvedValueOnce(row) + .mockResolvedValueOnce(row) + .mockResolvedValueOnce(undefined); + + await drainFastAgentParentEvents({ + conversationId: parent.sessionId, + eventKey: row.eventKey, + }); + + expect(mocks.recordWakeupOutcome).toHaveBeenCalledWith({ + id: 'wakeup-1', + status: 'succeeded', + }); + expect(mocks.recordAutomationOutcome).not.toHaveBeenCalled(); + }); + + it('counts a permanent delivery failure against the scheduled wakeup', async () => { + const wakeupEvent = { + type: 'scheduled_wakeup' as const, + eventId: 'wakeup-1:4', + wakeupId: 'wakeup-1', + name: 'Check PR #85', + prompt: 'Check whether PR #85 merged.', + runNumber: 4, + maxRuns: null, + firedAt: '2026-09-04T17:20:00.000Z', + nextRunAt: null, + reportPolicy: 'always' as const, + createdByUserId: 'user-1', + }; + const row = pendingRow('wakeup-event', wakeupEvent); + mocks.findPending + .mockResolvedValueOnce(row) + .mockResolvedValueOnce(row) + .mockResolvedValueOnce(undefined); + mocks.deliver.mockRejectedValueOnce( + new mocks.DeliveryError('parent session missing', { + replyPosted: false, + permanent: true, + }), + ); + + await drainFastAgentParentEvents({ + conversationId: parent.sessionId, + eventKey: row.eventKey, + }); + + expect(mocks.recordWakeupOutcome).toHaveBeenCalledWith({ + id: 'wakeup-1', + status: 'failed', + error: 'parent session missing', + }); + }); it('records success when delivery fails after a reply was posted', async () => { const launchClaimedAt = new Date('2026-09-01T15:18:41.782Z'); const automationEvent = { diff --git a/packages/sdk/src/server/lib/fast-agent-parent-event-queue.ts b/packages/sdk/src/server/lib/fast-agent-parent-event-queue.ts index 22d917531..e53e885d7 100644 --- a/packages/sdk/src/server/lib/fast-agent-parent-event-queue.ts +++ b/packages/sdk/src/server/lib/fast-agent-parent-event-queue.ts @@ -18,6 +18,7 @@ import { lte, or, recordCustomAutomationRunOutcome, + recordSessionWakeupOutcome, sql, taskRuns, } from '@roomote/db/server'; @@ -320,6 +321,22 @@ async function finalizeAutomationLaunch( }); } +async function finalizeScheduledWakeup( + event: FastAgentParentEvent, + status: 'succeeded' | 'failed', + error?: unknown, +) { + if (event.type !== 'scheduled_wakeup') return; + + await recordSessionWakeupOutcome({ + id: event.wakeupId, + status, + ...(status === 'failed' + ? { error: error instanceof Error ? error.message : String(error) } + : {}), + }); +} + /** Drain one parent's durable inbox in creation order under one turn lock. */ export async function drainFastAgentParentEvents( request: FastAgentParentEventQueueRequest, @@ -432,6 +449,7 @@ export async function drainFastAgentParentEvents( continue; } await finalizeAutomationLaunch(row.event, 'succeeded'); + await finalizeScheduledWakeup(row.event, 'succeeded'); await markDelivered(row.id); } catch (error) { if (findFastAgentDurableRetryScheduledError(error)) { @@ -447,11 +465,13 @@ export async function drainFastAgentParentEvents( error instanceof FastAgentParentEventDeliveryError ? error : null; if (deliveryError?.replyPosted) { await finalizeAutomationLaunch(row.event, 'succeeded'); + await finalizeScheduledWakeup(row.event, 'succeeded'); await markDelivered(row.id); continue; } if (deliveryError?.permanent) { await finalizeAutomationLaunch(row.event, 'failed', deliveryError); + await finalizeScheduledWakeup(row.event, 'failed', deliveryError); await markDiscarded(row.id, deliveryError); continue; } diff --git a/packages/sdk/src/server/lib/fast-agent-parent-event.test.ts b/packages/sdk/src/server/lib/fast-agent-parent-event.test.ts index 2af0427c8..12003c85f 100644 --- a/packages/sdk/src/server/lib/fast-agent-parent-event.test.ts +++ b/packages/sdk/src/server/lib/fast-agent-parent-event.test.ts @@ -17,6 +17,8 @@ const mocks = vi.hoisted(() => ({ findCustomAutomation: vi.fn(), findArtifacts: vi.fn(), findTaskRun: vi.fn(), + findWakeup: vi.fn(), + findWakeupSession: vi.fn(), findTaskRuns: vi.fn(), getConversationLookupIds: vi.fn(), findTaskPullRequests: vi.fn(), @@ -138,6 +140,8 @@ vi.mock('@roomote/db/server', () => ({ eq: vi.fn((...args: unknown[]) => args), inArray: vi.fn((...args: unknown[]) => args), getCustomAutomationById: mocks.findCustomAutomation, + getSessionWakeupById: mocks.findWakeup, + getSessionForFastConversation: mocks.findWakeupSession, slackInstallations: { isActive: 'slack_installations.is_active', teamId: 'slack_installations.team_id', @@ -2735,6 +2739,135 @@ describe('deliverFastAgentParentEvent', () => { expect(mocks.releaseTurnLock).toHaveBeenCalledOnce(); }); + it('skips a scheduled wakeup that was cancelled after its occurrence was admitted', async () => { + mocks.findWakeup.mockResolvedValueOnce({ status: 'cancelled' }); + const result = await deliverFastAgentParentEvent({ + parent, + event: { + type: 'scheduled_wakeup', + eventId: 'wakeup-1:1', + wakeupId: 'wakeup-1', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + runNumber: 1, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: null, + reportPolicy: 'always', + createdByUserId: 'user-1', + }, + }); + + expect(result).toBe('skipped'); + expect(mocks.answerQuestion).not.toHaveBeenCalled(); + expect(mocks.releaseTurnLock).toHaveBeenCalledOnce(); + }); + + it('skips a scheduled wakeup whose Session was archived even if the row is still active', async () => { + mocks.findWakeup.mockResolvedValueOnce({ status: 'active' }); + mocks.findWakeupSession.mockResolvedValueOnce({ + archivedAt: new Date('2026-09-04T17:05:00.000Z'), + }); + const result = await deliverFastAgentParentEvent({ + parent, + event: { + type: 'scheduled_wakeup', + eventId: 'wakeup-1:1', + wakeupId: 'wakeup-1', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + runNumber: 1, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: null, + reportPolicy: 'always', + createdByUserId: 'user-1', + }, + }); + + expect(result).toBe('skipped'); + expect(mocks.answerQuestion).not.toHaveBeenCalled(); + }); + + it('drops the reply and cancels the turn when the wakeup is cancelled mid-turn', async () => { + // Deliverable at the start of the turn, cancelled by the time the model + // wants to post. + mocks.findWakeup + .mockResolvedValueOnce({ status: 'active' }) + .mockResolvedValueOnce({ status: 'cancelled' }); + mocks.findWakeupSession.mockResolvedValue({ archivedAt: null }); + let signalAbortedAfterPost: boolean | undefined; + mocks.answerQuestion.mockImplementationOnce( + async ({ + adapter, + signal, + }: { + adapter: { postReply: (reply: unknown) => Promise }; + signal: AbortSignal; + }) => { + expect(signal.aborted).toBe(false); + await adapter.postReply({ + purpose: 'closeout', + message: 'Time to check the deploy.', + }); + signalAbortedAfterPost = signal.aborted; + return 'Time to check the deploy.'; + }, + ); + + await deliverFastAgentParentEvent({ + parent, + event: { + type: 'scheduled_wakeup', + eventId: 'wakeup-1:1', + wakeupId: 'wakeup-1', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + runNumber: 1, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: null, + reportPolicy: 'always', + createdByUserId: 'user-1', + }, + }); + + expect(mocks.postMessage).not.toHaveBeenCalled(); + expect(signalAbortedAfterPost).toBe(true); + }); + + it('still runs a scheduled wakeup whose one-shot row completed at claim time', async () => { + mocks.findWakeup.mockResolvedValueOnce({ status: 'completed' }); + mocks.findWakeupSession.mockResolvedValueOnce({ archivedAt: null }); + const result = await deliverFastAgentParentEvent({ + parent, + event: { + type: 'scheduled_wakeup', + eventId: 'wakeup-1:1', + wakeupId: 'wakeup-1', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + runNumber: 1, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: null, + reportPolicy: 'always', + createdByUserId: 'user-1', + }, + }); + + expect(result).toBe('delivered'); + expect(mocks.answerQuestion).toHaveBeenCalledOnce(); + expect(mocks.answerQuestion).toHaveBeenCalledWith( + expect.objectContaining({ + turnSource: 'platform_event', + platformEventKind: 'scheduled_wakeup', + platformEventVisibility: 'required', + userId: 'user-1', + }), + ); + }); + it('answers a pull request mention routed into a Slack Session on both the thread and the pull request', async () => { mocks.buildSourceControlFastDelivery.mockResolvedValue({ postComment: async (input: unknown) => { diff --git a/packages/sdk/src/server/lib/fast-agent-parent-event.ts b/packages/sdk/src/server/lib/fast-agent-parent-event.ts index e1c7ccd2f..fef0e0e5b 100644 --- a/packages/sdk/src/server/lib/fast-agent-parent-event.ts +++ b/packages/sdk/src/server/lib/fast-agent-parent-event.ts @@ -22,6 +22,8 @@ import { customAutomations, eq, getCustomAutomationById, + getSessionForFastConversation, + getSessionWakeupById, inArray, slackInstallations, taskArtifacts, @@ -54,6 +56,7 @@ import { exitedRunStatuses, type FastAgentConversation, type FastAgentHumanFollowUpEvent, + type FastAgentScheduledWakeupEvent, type FastAgentSourceControlReplyTarget, type FastAgentParent, type PullRequestStatus, @@ -167,6 +170,7 @@ export type FastAgentPullRequestContext = { export type FastAgentParentEvent = | FastAgentHumanFollowUpEvent + | FastAgentScheduledWakeupEvent | { type: 'automation_triggered'; eventId: string; @@ -368,6 +372,8 @@ export function buildEventClientMessageSeed( return `fast-parent-human-follow-up:${event.eventId}`; case 'automation_triggered': return `fast-parent-automation:${event.eventId}`; + case 'scheduled_wakeup': + return `fast-parent-wakeup:${event.eventId}`; case 'child_message': return `fast-parent-child-message:${event.messageId}`; case 'artifact_published': @@ -2167,12 +2173,98 @@ export async function deliverFastAgentParentEvent( * conversation. The caller owns lock release and may invoke this repeatedly * to preserve durable queue order without letting another turn interleave. */ +/** + * Whether a scheduled wakeup may still speak. The row is authoritative: a + * cancelled or failed wakeup must not run, and an archived Session must stay + * quiet even if its cancel-on-archive step failed. A row that is already + * `completed` is fine, because the claim that completes a one-shot or final + * run happens before delivery. + */ +async function isScheduledWakeupDeliverable(params: { + wakeupId: string; + conversationId: string; +}): Promise { + const [wakeup, session] = await Promise.all([ + getSessionWakeupById(params.wakeupId), + getSessionForFastConversation(db, params.conversationId), + ]); + return Boolean( + wakeup && + wakeup.status !== 'cancelled' && + wakeup.status !== 'failed' && + !session?.archivedAt, + ); +} + +type ScheduledWakeupReplyGuard = { + signal: AbortSignal; + guardPostReply: ( + postReply: FastAgentTurnAdapter['postReply'], + ) => FastAgentTurnAdapter['postReply']; +}; + +/** + * Wrap a wakeup turn's reply path so the wakeup is re-checked immediately + * before anything user-visible goes out. If it was cancelled or its Session + * archived while the model was working, the post is dropped and the turn's + * signal is aborted so no further tool calls run. The next drain of the + * event sees the same state and settles it as skipped. + */ +function createScheduledWakeupReplyGuard(params: { + wakeupId: string; + conversationId: string; + upstream: AbortSignal; +}): ScheduledWakeupReplyGuard { + const controller = new AbortController(); + const abortFromUpstream = () => controller.abort(params.upstream.reason); + if (params.upstream.aborted) { + abortFromUpstream(); + } else { + params.upstream.addEventListener('abort', abortFromUpstream, { + once: true, + }); + } + return { + signal: controller.signal, + guardPostReply: (postReply) => async (reply) => { + if ( + !controller.signal.aborted && + (await isScheduledWakeupDeliverable(params)) + ) { + return postReply(reply); + } + if (!controller.signal.aborted) { + console.warn( + `[SessionWakeups] Dropped a reply for wakeup ${params.wakeupId}: it was cancelled or its Session archived while the turn was running.`, + ); + controller.abort( + new Error( + 'Scheduled wakeup was cancelled or its Session archived while the turn was running.', + ), + ); + } + return undefined; + }, + }; +} + export async function deliverFastAgentParentEventWithLock( params: FastAgentParentEventDeliveryParams, turnLock: FastAgentTurnLockHandle, ): Promise<'delivered' | 'skipped'> { let replyPosted = false; - const turnSignal = turnLock.signal; + // A wakeup turn revalidates at reply time as well as at start: a cancel or + // archive that lands while the model is generating must still win, so the + // guard suppresses the post and cancels the rest of the turn. + const wakeupGuard = + params.event.type === 'scheduled_wakeup' + ? createScheduledWakeupReplyGuard({ + wakeupId: params.event.wakeupId, + conversationId: params.parent.sessionId, + upstream: turnLock.signal, + }) + : null; + const turnSignal = wakeupGuard?.signal ?? turnLock.signal; try { if (params.event.type === 'pull_request_opened') { @@ -2184,13 +2276,31 @@ export async function deliverFastAgentParentEventWithLock( return 'skipped'; } } + // A wakeup can be cancelled (or its Session archived) after its + // occurrence was admitted here but before this turn runs. The row is + // authoritative: a cancelled or failed wakeup must not speak. A row that + // is already `completed` is fine, because the claim that completes a + // one-shot or final run happens before delivery. + if ( + params.event.type === 'scheduled_wakeup' && + !(await isScheduledWakeupDeliverable({ + wakeupId: params.event.wakeupId, + conversationId: params.parent.sessionId, + })) + ) { + return 'skipped'; + } const humanFollowUp = params.event.type === 'human_follow_up' ? params.event : null; const parentTurn = await createFastAgentParentTurn({ parent: params.parent, event: params.event, - ...(humanFollowUp ? { actorUserId: humanFollowUp.userId } : {}), + ...(humanFollowUp + ? { actorUserId: humanFollowUp.userId } + : params.event.type === 'scheduled_wakeup' + ? { actorUserId: params.event.createdByUserId } + : {}), onReplyPosted: () => { replyPosted = true; }, @@ -2277,14 +2387,18 @@ export async function deliverFastAgentParentEventWithLock( humanFollowUp?.platformEventVisibility ?? (params.event.type === 'pull_request_feedback' || params.event.type === 'pull_request_conflict_detected' || - params.event.type === 'automation_triggered' + params.event.type === 'automation_triggered' || + (params.event.type === 'scheduled_wakeup' && + params.event.reportPolicy === 'always') ? 'required' : 'optional'), platformEventKind: humanFollowUp?.platformEventKind ?? (params.event.type === 'automation_triggered' ? 'automation' - : 'delegated_task'), + : params.event.type === 'scheduled_wakeup' + ? 'scheduled_wakeup' + : 'delegated_task'), automationReport: params.event.type === 'task_settled' && Boolean(params.event.customAutomationId), @@ -2309,6 +2423,13 @@ export async function deliverFastAgentParentEventWithLock( createArtifact: buildFastAgentArtifactCreator(params.parent.sessionId), ...parentTurn.adapter, launchTask: parentTurn.adapter.launchTask, + ...(wakeupGuard + ? { + postReply: wakeupGuard.guardPostReply( + parentTurn.adapter.postReply, + ), + } + : {}), resolveMcpServerConfigs: () => resolveUserMcpServerConfigs({ userId: parentTurn.userId, diff --git a/packages/sdk/src/server/lib/session-wakeups.test.ts b/packages/sdk/src/server/lib/session-wakeups.test.ts new file mode 100644 index 000000000..168c3bca7 --- /dev/null +++ b/packages/sdk/src/server/lib/session-wakeups.test.ts @@ -0,0 +1,244 @@ +const mocks = vi.hoisted(() => ({ + enqueueFire: vi.fn(), + findById: vi.fn(), + resolveNextRun: vi.fn(), + claimFire: vi.fn(), + getById: vi.fn(), + listDue: vi.fn(), + recordOutcome: vi.fn(), + enqueueParentEvent: vi.fn(), +})); + +vi.mock('@roomote/cloud-agents/server', () => ({ + SESSION_WAKEUP_FIRE_JOB_NAME: 'fire', + SESSION_WAKEUP_QUEUE_NAME: 'session-wakeups', + enqueueSessionWakeupFire: mocks.enqueueFire, + fastAgentConversationRepository: { findById: mocks.findById }, + resolveSessionWakeupNextRun: mocks.resolveNextRun, +})); + +vi.mock('@roomote/db/server', () => ({ + claimSessionWakeupFire: mocks.claimFire, + getSessionWakeupById: mocks.getById, + listDueSessionWakeups: mocks.listDue, + recordSessionWakeupOutcome: mocks.recordOutcome, +})); + +vi.mock('./fast-agent-parent-event-queue', () => ({ + enqueueFastAgentParentEvent: mocks.enqueueParentEvent, +})); + +import { + fireSessionWakeup, + recoverPendingSessionWakeups, +} from './session-wakeups'; + +const conversation = { + surface: 'web' as const, + workspaceId: 'deployment', + conversationId: 'conversation-1', +}; + +const nextRunAt = new Date('2026-09-04T17:10:00.000Z'); + +function activeRow(overrides: Record = {}) { + return { + id: 'wakeup-1', + conversationId: '11111111-1111-4111-8111-111111111111', + createdByUserId: 'user-1', + name: 'Check PR #85', + prompt: 'Check whether PR #85 merged.', + promptSignature: 'check whether pr #85 merged.', + schedule: { mode: 'interval', everyMinutes: 10 }, + reportPolicy: 'only_when_notable', + status: 'active', + runCount: 2, + maxRuns: null, + until: null, + consecutiveFailures: 0, + nextRunAt, + lastFiredAt: null, + lastError: null, + completedAt: null, + createdAt: new Date('2026-09-04T16:00:00.000Z'), + updatedAt: new Date('2026-09-04T16:00:00.000Z'), + ...overrides, + }; +} + +describe('fireSessionWakeup', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.spyOn(console, 'log').mockImplementation(() => {}); + mocks.findById.mockResolvedValue({ userId: 'owner-1', conversation }); + mocks.enqueueParentEvent.mockResolvedValue({ eventKey: 'k', queued: true }); + mocks.enqueueFire.mockResolvedValue(undefined); + mocks.recordOutcome.mockResolvedValue(null); + }); + + it('admits the event before claiming, then schedules the next occurrence', async () => { + const row = activeRow(); + const following = new Date('2026-09-04T17:20:00.000Z'); + mocks.getById.mockResolvedValue(row); + mocks.resolveNextRun.mockReturnValue(following); + mocks.claimFire.mockResolvedValue({ ...row, runCount: 3 }); + + const result = await fireSessionWakeup({ + wakeupId: row.id, + runAt: nextRunAt.getTime(), + }); + + expect(result).toEqual({ + outcome: 'fired', + eventId: 'wakeup-1:3', + nextRunAt: following, + }); + expect(mocks.enqueueParentEvent).toHaveBeenCalledWith({ + parent: { sessionId: row.conversationId, conversation }, + event: expect.objectContaining({ + type: 'scheduled_wakeup', + eventId: 'wakeup-1:3', + wakeupId: 'wakeup-1', + runNumber: 3, + prompt: row.prompt, + reportPolicy: 'only_when_notable', + createdByUserId: 'user-1', + nextRunAt: following.toISOString(), + }), + }); + expect(mocks.enqueueParentEvent.mock.invocationCallOrder[0]).toBeLessThan( + mocks.claimFire.mock.invocationCallOrder[0]!, + ); + expect(mocks.claimFire).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'wakeup-1', + expectedNextRunAt: nextRunAt, + nextRunAt: following, + }), + ); + expect(mocks.enqueueFire).toHaveBeenCalledWith({ + wakeupId: 'wakeup-1', + runAt: following.getTime(), + }); + }); + + it('skips a stale job whose occurrence already advanced', async () => { + mocks.getById.mockResolvedValue(activeRow()); + + const result = await fireSessionWakeup({ + wakeupId: 'wakeup-1', + runAt: nextRunAt.getTime() - 60_000, + }); + + expect(result.outcome).toBe('skipped'); + expect(mocks.enqueueParentEvent).not.toHaveBeenCalled(); + expect(mocks.claimFire).not.toHaveBeenCalled(); + }); + + it('skips terminal wakeups without touching the conversation', async () => { + mocks.getById.mockResolvedValue( + activeRow({ status: 'cancelled', nextRunAt: null }), + ); + + const result = await fireSessionWakeup({ + wakeupId: 'wakeup-1', + runAt: nextRunAt.getTime(), + }); + + expect(result).toEqual({ + outcome: 'skipped', + reason: 'Wakeup is cancelled.', + }); + expect(mocks.findById).not.toHaveBeenCalled(); + }); + + it('does not schedule a follow-up when another worker won the claim', async () => { + mocks.getById.mockResolvedValue(activeRow()); + mocks.resolveNextRun.mockReturnValue(new Date('2026-09-04T17:20:00.000Z')); + mocks.claimFire.mockResolvedValue(null); + + const result = await fireSessionWakeup({ + wakeupId: 'wakeup-1', + runAt: nextRunAt.getTime(), + }); + + expect(result.outcome).toBe('skipped'); + expect(mocks.enqueueParentEvent).toHaveBeenCalledOnce(); + expect(mocks.enqueueFire).not.toHaveBeenCalled(); + }); + + it('completes a once wakeup after its only run', async () => { + const row = activeRow({ + schedule: { mode: 'once', at: nextRunAt.toISOString() }, + reportPolicy: 'always', + runCount: 0, + }); + mocks.getById.mockResolvedValue(row); + mocks.resolveNextRun.mockReturnValue(null); + mocks.claimFire.mockResolvedValue({ ...row, status: 'completed' }); + + const result = await fireSessionWakeup({ + wakeupId: row.id, + runAt: nextRunAt.getTime(), + }); + + expect(result).toEqual({ + outcome: 'fired', + eventId: 'wakeup-1:1', + nextRunAt: null, + }); + expect(mocks.enqueueParentEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ nextRunAt: null, maxRuns: null }), + }), + ); + expect(mocks.enqueueFire).not.toHaveBeenCalled(); + }); + + it('records a failure and advances when the conversation is gone', async () => { + const row = activeRow(); + mocks.getById.mockResolvedValue(row); + mocks.findById.mockResolvedValue(null); + mocks.resolveNextRun.mockReturnValue(new Date('2026-09-04T17:20:00.000Z')); + mocks.claimFire.mockResolvedValue(row); + + const result = await fireSessionWakeup({ + wakeupId: row.id, + runAt: nextRunAt.getTime(), + }); + + expect(result.outcome).toBe('skipped'); + expect(mocks.enqueueParentEvent).not.toHaveBeenCalled(); + expect(mocks.recordOutcome).toHaveBeenCalledWith( + expect.objectContaining({ id: 'wakeup-1', status: 'failed' }), + ); + expect(mocks.enqueueFire).toHaveBeenCalledOnce(); + }); +}); + +describe('recoverPendingSessionWakeups', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + it('re-adds a hint for every due row and survives one failure', async () => { + mocks.listDue.mockResolvedValue([ + { id: 'a', nextRunAt: new Date('2026-09-04T17:00:00.000Z') }, + { id: 'b', nextRunAt: new Date('2026-09-04T17:01:00.000Z') }, + { id: 'c', nextRunAt: null }, + ]); + mocks.enqueueFire + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('redis restarting')); + + await expect( + recoverPendingSessionWakeups(new Date('2026-09-04T17:00:30.000Z')), + ).resolves.toBe(1); + + expect(mocks.listDue).toHaveBeenCalledWith({ + dueBy: new Date('2026-09-04T17:02:30.000Z'), + }); + expect(mocks.enqueueFire).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/sdk/src/server/lib/session-wakeups.ts b/packages/sdk/src/server/lib/session-wakeups.ts new file mode 100644 index 000000000..a55b9f437 --- /dev/null +++ b/packages/sdk/src/server/lib/session-wakeups.ts @@ -0,0 +1,190 @@ +import { + enqueueSessionWakeupFire, + fastAgentConversationRepository, + resolveSessionWakeupNextRun, + type SessionWakeupFireJob, +} from '@roomote/cloud-agents/server'; +import { + claimSessionWakeupFire, + getSessionWakeupById, + listDueSessionWakeups, + recordSessionWakeupOutcome, +} from '@roomote/db/server'; +import { + FAST_AGENT_SCHEDULED_WAKEUP_EVENT_TYPE, + type FastAgentScheduledWakeupEvent, +} from '@roomote/types'; + +import { enqueueFastAgentParentEvent } from './fast-agent-parent-event-queue'; + +export { + SESSION_WAKEUP_FIRE_JOB_NAME, + SESSION_WAKEUP_QUEUE_NAME, + type SessionWakeupFireJob, +} from '@roomote/cloud-agents/server'; + +const LOG_PREFIX = '[SessionWakeups]'; +/** + * The recovery sweep re-adds delayed hints for rows due within this window, + * so a hint lost to a Redis restart shortly before its time still fires on + * time rather than one sweep late. + */ +export const SESSION_WAKEUP_RECOVERY_LOOKAHEAD_MS = 2 * 60_000; + +export type FireSessionWakeupResult = + | { outcome: 'fired'; eventId: string; nextRunAt: Date | null } + | { outcome: 'skipped'; reason: string }; + +/** + * Fire one occurrence of a wakeup: admit the platform event into the + * conversation's durable inbox, then claim the occurrence on the row and + * schedule the next one. + * + * The event is admitted before the claim. Its key derives from the wakeup id + * and run number, so a crash between the two steps leaves a pending event + * and an unadvanced row; the recovery sweep re-fires, the duplicate + * admission is a no-op on its key, and the claim then succeeds. The reverse + * order could lose an occurrence. + */ +export async function fireSessionWakeup( + job: SessionWakeupFireJob, +): Promise { + const row = await getSessionWakeupById(job.wakeupId); + if (!row) return { outcome: 'skipped', reason: 'Wakeup no longer exists.' }; + if (row.status !== 'active' || !row.nextRunAt) { + return { outcome: 'skipped', reason: `Wakeup is ${row.status}.` }; + } + if (row.nextRunAt.getTime() !== job.runAt) { + return { + outcome: 'skipped', + reason: 'Occurrence already advanced past this job.', + }; + } + + const firedAt = new Date(); + const runNumber = row.runCount + 1; + const nextRunAt = resolveSessionWakeupNextRun({ + schedule: row.schedule, + firedAt, + runCountAfterFire: runNumber, + maxRuns: row.maxRuns, + until: row.until, + }); + + const record = await fastAgentConversationRepository.findById({ + id: row.conversationId, + }); + if (!record) { + // Claim first so the row does not fire again every sweep, then count + // the failure so a conversation that stays missing retires the wakeup. + await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: row.nextRunAt, + nextRunAt, + firedAt, + }); + await recordSessionWakeupOutcome({ + id: row.id, + status: 'failed', + error: 'The Fast conversation for this wakeup was not found.', + }); + if (nextRunAt) { + await enqueueSessionWakeupFire({ + wakeupId: row.id, + runAt: nextRunAt.getTime(), + }); + } + return { outcome: 'skipped', reason: 'Conversation not found.' }; + } + + const event: FastAgentScheduledWakeupEvent = { + type: FAST_AGENT_SCHEDULED_WAKEUP_EVENT_TYPE, + eventId: `${row.id}:${runNumber}`, + wakeupId: row.id, + name: row.name, + prompt: row.prompt, + runNumber, + maxRuns: row.maxRuns, + firedAt: firedAt.toISOString(), + nextRunAt: nextRunAt?.toISOString() ?? null, + reportPolicy: row.reportPolicy, + createdByUserId: row.createdByUserId ?? record.userId ?? '', + }; + if (!event.createdByUserId) { + await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: row.nextRunAt, + nextRunAt: null, + firedAt, + }); + await recordSessionWakeupOutcome({ + id: row.id, + status: 'failed', + error: 'No user remains to act on this wakeup.', + }); + return { outcome: 'skipped', reason: 'No acting user.' }; + } + + await enqueueFastAgentParentEvent({ + parent: { + sessionId: row.conversationId, + conversation: record.conversation, + }, + event, + }); + + const claimed = await claimSessionWakeupFire({ + id: row.id, + expectedNextRunAt: row.nextRunAt, + nextRunAt, + firedAt, + }); + if (!claimed) { + return { + outcome: 'skipped', + reason: 'Another worker claimed this occurrence.', + }; + } + + if (nextRunAt) { + await enqueueSessionWakeupFire({ + wakeupId: row.id, + runAt: nextRunAt.getTime(), + }); + } + + console.log( + `${LOG_PREFIX} Fired "${row.name}" (${row.id}) run #${runNumber}; next ${nextRunAt ? nextRunAt.toISOString() : 'none'}.`, + ); + return { outcome: 'fired', eventId: event.eventId, nextRunAt }; +} + +/** + * Recreate delayed hints for active rows that are due or nearly due. Runs on + * the queue's recovery schedule and at worker start so Redis restarts and + * lost jobs cannot strand a wakeup; the compare-and-set claim keeps a + * duplicate hint harmless. + */ +export async function recoverPendingSessionWakeups( + now = new Date(), +): Promise { + const rows = await listDueSessionWakeups({ + dueBy: new Date(now.getTime() + SESSION_WAKEUP_RECOVERY_LOOKAHEAD_MS), + }); + let recovered = 0; + for (const row of rows) { + if (!row.nextRunAt) continue; + try { + await enqueueSessionWakeupFire({ + wakeupId: row.id, + runAt: row.nextRunAt.getTime(), + }); + recovered += 1; + } catch (error) { + console.error( + `${LOG_PREFIX} Failed to re-add hint for wakeup ${row.id}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + return recovered; +} diff --git a/packages/types/src/fast-agent-tool-catalog.ts b/packages/types/src/fast-agent-tool-catalog.ts index a3f2ce5af..19dffb3cf 100644 --- a/packages/types/src/fast-agent-tool-catalog.ts +++ b/packages/types/src/fast-agent-tool-catalog.ts @@ -12,6 +12,7 @@ export const FAST_AGENT_NATIVE_TOOL_NAMES = { ignoreEvent: 'ignore_event', inspectImages: 'inspect_images', launchTask: 'launch_task', + manageWakeups: 'manage_wakeups', retryTaskStart: 'retry_task_start', saveMemory: 'save_memory', sendChatReaction: 'send_chat_reaction', @@ -56,6 +57,10 @@ export const FAST_AGENT_NATIVE_TOOL_CATALOG = [ kind: ACP_TOOL_KINDS.read, }, { name: FAST_AGENT_NATIVE_TOOL_NAMES.launchTask, kind: ACP_TOOL_KINDS.task }, + { + name: FAST_AGENT_NATIVE_TOOL_NAMES.manageWakeups, + kind: ACP_TOOL_KINDS.task, + }, { name: FAST_AGENT_NATIVE_TOOL_NAMES.retryTaskStart, kind: ACP_TOOL_KINDS.task, diff --git a/packages/types/src/fast-agent.ts b/packages/types/src/fast-agent.ts index 7bc116136..fc2418c88 100644 --- a/packages/types/src/fast-agent.ts +++ b/packages/types/src/fast-agent.ts @@ -223,6 +223,7 @@ export const fastAgentPlatformEventKindSchema = z.enum([ 'automation', 'setup', 'input_response', + 'scheduled_wakeup', ]); export const fastAgentPlatformEventVisibilitySchema = z.enum([ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 407742f77..0fa1776fd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -12,6 +12,7 @@ export * from './cloud-agents'; export * from './pr-review-action'; export * from './task-runs'; export * from './sessions'; +export * from './session-wakeups'; export * from './fast-agent'; export * from './fast-agent-tool-catalog'; export * from './integration-tool-lookup'; diff --git a/packages/types/src/session-wakeups.test.ts b/packages/types/src/session-wakeups.test.ts new file mode 100644 index 000000000..7c98901c8 --- /dev/null +++ b/packages/types/src/session-wakeups.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import { + FAST_AGENT_NATIVE_TOOL_NAMES, + getFastAgentNativeAcpKind, +} from './fast-agent-tool-catalog'; +import { + MANAGE_WAKEUPS_ACTIONS, + MANAGE_WAKEUPS_TOOL, + fastAgentScheduledWakeupEventSchema, + manageWakeupsInputSchema, + sessionWakeupScheduleSchema, +} from './session-wakeups'; + +describe('manage wakeups tool contract', () => { + it('accepts optional persisted relative identity without requiring it on legacy rows', () => { + const absolute = { mode: 'once', at: '2026-09-04T17:02:00.000Z' }; + expect(sessionWakeupScheduleSchema.parse(absolute)).toEqual(absolute); + expect( + sessionWakeupScheduleSchema.parse({ ...absolute, inMinutes: 2 }), + ).toEqual({ ...absolute, inMinutes: 2 }); + for (const inMinutes of [0, -1, 1.5, 43_201, '2', null]) { + expect( + sessionWakeupScheduleSchema.safeParse({ ...absolute, inMinutes }) + .success, + ).toBe(false); + } + }); + it('keeps every supported action in the shared Zod schema', () => { + for (const action of MANAGE_WAKEUPS_ACTIONS) { + expect(manageWakeupsInputSchema.parse({ action })).toEqual({ action }); + } + }); + + it('publishes the canonical descriptor and is a Fast native tool', () => { + expect(MANAGE_WAKEUPS_TOOL.name).toBe('manage_wakeups'); + expect(FAST_AGENT_NATIVE_TOOL_NAMES.manageWakeups).toBe( + MANAGE_WAKEUPS_TOOL.name, + ); + expect(getFastAgentNativeAcpKind('manage_wakeups')).toBe('task'); + expect(MANAGE_WAKEUPS_TOOL.description).toContain('"in 20m"'); + expect(MANAGE_WAKEUPS_TOOL.description).toContain('There is no pause.'); + expect(MANAGE_WAKEUPS_TOOL.description).toContain( + 'Never poll, sleep, or wait', + ); + expect(MANAGE_WAKEUPS_TOOL.inputSchema.schedule.description).toContain( + 'cron 0 9 * * 1-5 America/New_York', + ); + }); + + it('takes the schedule as one string and nothing else schedule-shaped', () => { + expect(Object.keys(MANAGE_WAKEUPS_TOOL.inputSchema).sort()).toEqual([ + 'action', + 'name', + 'prompt', + 'reportPolicy', + 'schedule', + 'wakeupId', + ]); + expect( + manageWakeupsInputSchema.parse({ + action: 'create', + name: 'Check the deploy', + prompt: 'Tell the user to check the deploy.', + schedule: ' in 2m ', + }).schedule, + ).toBe('in 2m'); + expect( + manageWakeupsInputSchema.safeParse({ + action: 'create', + schedule: { mode: 'once', inMinutes: 2 }, + }).success, + ).toBe(false); + }); + + it('validates the scheduled wakeup platform event', () => { + expect( + fastAgentScheduledWakeupEventSchema.safeParse({ + type: 'scheduled_wakeup', + eventId: 'wakeup-1:3', + wakeupId: 'wakeup-1', + name: 'Check PR #85', + prompt: 'Check whether PR #85 merged.', + runNumber: 3, + maxRuns: null, + firedAt: '2026-09-04T17:10:00.000Z', + nextRunAt: '2026-09-04T17:20:00.000Z', + reportPolicy: 'only_when_notable', + createdByUserId: 'user-1', + }).success, + ).toBe(true); + }); +}); diff --git a/packages/types/src/session-wakeups.ts b/packages/types/src/session-wakeups.ts new file mode 100644 index 000000000..9546409c2 --- /dev/null +++ b/packages/types/src/session-wakeups.ts @@ -0,0 +1,218 @@ +import { z } from 'zod'; + +/** + * Session wakeups let a Fast conversation schedule a message back to itself. + * When a wakeup fires, the platform injects a `scheduled_wakeup` event into + * the same conversation and the agent handles it as a normal turn with the + * full conversation still in context. A one-shot wakeup is a reminder; a + * recurring wakeup is a monitor. + * + * The tool takes the schedule as one short string ("in 2m", "every 10m x3", + * "cron 0 9 * * 1-5 America/New_York") rather than a structured union. + * Models fill every optional structured field with placeholders, and each + * rejected placeholder costs a retry; a single required string has nothing to + * pad. + */ + +export const SESSION_WAKEUP_NAME_MAX_LENGTH = 80; +export const SESSION_WAKEUP_NAME_MIN_LENGTH = 3; +export const SESSION_WAKEUP_PROMPT_MAX_LENGTH = 4_000; +export const SESSION_WAKEUP_PROMPT_MIN_LENGTH = 10; +export const SESSION_WAKEUP_SCHEDULE_MAX_LENGTH = 160; +/** Active wakeups one conversation may hold at once. */ +export const MAX_ACTIVE_SESSION_WAKEUPS = 10; +export const SESSION_WAKEUP_MIN_INTERVAL_MINUTES = 1; +/** Seven days. */ +export const SESSION_WAKEUP_MAX_INTERVAL_MINUTES = 7 * 24 * 60; +/** + * Recurring wakeups tighter than this must carry a run count or an end time, + * so a tight polling loop cannot run forever. + */ +export const SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES = 5; +/** Thirty days. A one-shot wakeup may not be scheduled further out. */ +export const SESSION_WAKEUP_MAX_ONCE_HORIZON_MINUTES = 30 * 24 * 60; +export const SESSION_WAKEUP_MAX_RUNS_LIMIT = 1_000; +/** A recurring wakeup whose turns fail this many times in a row is retired. */ +export const SESSION_WAKEUP_MAX_CONSECUTIVE_FAILURES = 5; + +export const SESSION_WAKEUP_STATUSES = [ + 'active', + 'completed', + 'cancelled', + 'failed', +] as const; +export type SessionWakeupStatus = (typeof SESSION_WAKEUP_STATUSES)[number]; + +export const SESSION_WAKEUP_REPORT_POLICIES = [ + 'always', + 'only_when_notable', +] as const; +export type SessionWakeupReportPolicy = + (typeof SESSION_WAKEUP_REPORT_POLICIES)[number]; + +/** The normalized schedule persisted with a wakeup. */ +export const sessionWakeupScheduleSchema = z.discriminatedUnion('mode', [ + z + .object({ + mode: z.literal('once'), + at: z.string(), + // Stable relative identity for retries; at remains the firing time. + inMinutes: z + .number() + .int() + .positive() + .max(SESSION_WAKEUP_MAX_ONCE_HORIZON_MINUTES) + .optional(), + }) + .strict(), + z + .object({ mode: z.literal('interval'), everyMinutes: z.number().int() }) + .strict(), + z + .object({ + mode: z.literal('cron'), + expression: z.string(), + timezone: z.string(), + }) + .strict(), +]); + +export type SessionWakeupSchedule = z.infer; + +export function isSessionWakeupRecurring( + schedule: Pick, +): boolean { + return schedule.mode !== 'once'; +} + +export const SESSION_WAKEUP_SCHEDULE_GRAMMAR = `One of: +- "in m|h|d" for a one-shot delay, e.g. "in 2m", "in 90m", "in 3h" (preferred for reminders and delayed follow-ups) +- "at " for a one-shot at an absolute time, e.g. "at 2026-09-04T15:00:00-04:00" +- "every m|h|d" for a repeating interval, e.g. "every 10m", "every 6h"; add "x" to stop after that many runs ("every 1m x3") or "until " to stop after a time ("every 10m until 2026-09-04T18:00:00Z") +- "cron [IANA timezone]" for a calendar schedule, e.g. "cron 0 9 * * 1-5 America/New_York" (timezone defaults to the deployment timezone); "x" and "until " work here too, and a cron that fires more often than every 5 minutes requires one of them`; + +export const MANAGE_WAKEUPS_ACTIONS = [ + 'create', + 'list', + 'get', + 'cancel', +] as const; +export type ManageWakeupsAction = (typeof MANAGE_WAKEUPS_ACTIONS)[number]; + +export const manageWakeupsFieldSchemas = { + action: z + .enum(MANAGE_WAKEUPS_ACTIONS) + .describe( + 'create schedules a new wakeup; list shows the active wakeups in this conversation; get shows one by id; cancel stops one. Cancel is the only stop action.', + ), + wakeupId: z + .string() + .trim() + .min(1) + .optional() + .describe('Required for get and cancel. Omit otherwise.'), + name: z + .string() + .trim() + .min(SESSION_WAKEUP_NAME_MIN_LENGTH) + .max(SESSION_WAKEUP_NAME_MAX_LENGTH) + .optional() + .describe( + '[create] Short label for the wakeup, for example "Check PR #85 for merge".', + ), + prompt: z + .string() + .trim() + .min(SESSION_WAKEUP_PROMPT_MIN_LENGTH) + .max(SESSION_WAKEUP_PROMPT_MAX_LENGTH) + .optional() + .describe( + '[create] What to do when the wakeup fires. This conversation will still be in context, so keep it short and concrete. Say what to check, what counts as done, and what to tell the user.', + ), + schedule: z + .string() + .trim() + .min(1) + .max(SESSION_WAKEUP_SCHEDULE_MAX_LENGTH) + .optional() + .describe(`[create] ${SESSION_WAKEUP_SCHEDULE_GRAMMAR}`), + reportPolicy: z + .enum(SESSION_WAKEUP_REPORT_POLICIES) + .optional() + .describe( + '[create] "always" replies to the user on every run (default for one-shots). "only_when_notable" stays silent unless there is news or the condition resolved (default for repeating schedules). Omit to use the default.', + ), +} satisfies z.ZodRawShape; + +export const manageWakeupsInputSchema = z.object(manageWakeupsFieldSchemas); + +export type ManageWakeupsInput = z.infer; + +export const MANAGE_WAKEUPS_TOOL_NAME = 'manage_wakeups' as const; + +export const MANAGE_WAKEUPS_TOOL_DESCRIPTION = `Schedule this conversation to wake itself up later, once or on a cadence. When a wakeup fires, you receive a scheduled_wakeup platform event in this same conversation with the full history still in context, so the prompt can be brief and refer to things discussed here. Use it for reminders ("remind me in 20 minutes", "ping me at 3pm") and for monitors ("check every 10 minutes whether CI is green", "every weekday at 9am summarize open PRs"). + +The schedule is one short string. Reminders and delayed follow-ups use "in m" ("in 20m"); use "at " only for an explicit absolute time. Repeating checks use "every m" or "cron ...", optionally with "x" or "until ". Pick an interval that matches how fast the monitored thing actually changes, not how soon you want an answer; intervals under ${SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES} minutes need "x" or "until". + +- A monitor keeps running until the user cancels it or the condition definitively resolves. A run that finds nothing new is still useful. When a monitored condition resolves, tell the user and cancel the wakeup. +- Results arrive automatically as a new turn in this conversation. Never poll, sleep, or wait for a wakeup inside a turn. +- When the user says stop, cancel, remove, delete, or end a wakeup, use cancel. There is no pause. +- Creating a wakeup that matches an active one (same prompt and schedule) returns the existing wakeup instead of a duplicate. At most ${MAX_ACTIVE_SESSION_WAKEUPS} wakeups may be active per conversation. +- Only send the fields the action needs; omit the rest. After creating a wakeup, confirm what will happen and when in one short sentence using the returned nextRunAt.`; + +export const MANAGE_WAKEUPS_TOOL = { + name: MANAGE_WAKEUPS_TOOL_NAME, + title: 'Manage Wakeups', + description: MANAGE_WAKEUPS_TOOL_DESCRIPTION, + inputSchema: manageWakeupsFieldSchemas, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: false, + }, +} as const; + +/** Tool-facing view of a wakeup row. */ +export type SessionWakeupSummary = { + id: string; + name: string; + prompt: string; + schedule: SessionWakeupSchedule; + scheduleDescription: string; + reportPolicy: SessionWakeupReportPolicy; + status: SessionWakeupStatus; + runCount: number; + maxRuns: number | null; + until: string | null; + nextRunAt: string | null; + lastFiredAt: string | null; + lastError: string | null; + createdAt: string; +}; + +export const FAST_AGENT_SCHEDULED_WAKEUP_EVENT_TYPE = + 'scheduled_wakeup' as const; + +/** + * The parent event a firing wakeup admits into its conversation. The prompt + * is repeated here so the turn does not depend on the row still existing. + */ +export const fastAgentScheduledWakeupEventSchema = z.object({ + type: z.literal(FAST_AGENT_SCHEDULED_WAKEUP_EVENT_TYPE), + /** `${wakeupId}:${runNumber}`; one admission per occurrence. */ + eventId: z.string().min(1), + wakeupId: z.string().min(1), + name: z.string().min(1), + prompt: z.string().min(1), + runNumber: z.number().int().min(1), + maxRuns: z.number().int().nullable(), + firedAt: z.string().min(1), + nextRunAt: z.string().nullable(), + reportPolicy: z.enum(SESSION_WAKEUP_REPORT_POLICIES), + createdByUserId: z.string().min(1), +}); + +export type FastAgentScheduledWakeupEvent = z.infer< + typeof fastAgentScheduledWakeupEventSchema +>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e37dd3d25..e11e5dd4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1164,6 +1164,12 @@ importers: ai: specifier: ^6.0.116 version: 6.0.116(zod@3.25.76) + bullmq: + specifier: ^5.78.0 + version: 5.78.0 + cron-parser: + specifier: 5.6.1 + version: 5.6.1 dompurify: specifier: 3.4.13 version: 3.4.13