diff --git a/apps/web/lib/components/notifications/notification-config.tsx b/apps/web/lib/components/notifications/notification-config.tsx index d5418dd9a..46eaee633 100644 --- a/apps/web/lib/components/notifications/notification-config.tsx +++ b/apps/web/lib/components/notifications/notification-config.tsx @@ -9,6 +9,7 @@ import { IconUserPlus, IconMessage, IconPlayerPlay, + IconAlertTriangle, } from "@tabler/icons-react"; import { Avatar, AvatarFallback } from "@conductor/ui"; import type { BadgeProps } from "@conductor/ui"; @@ -71,6 +72,13 @@ export const typeConfig: Record< iconBg: "bg-success/10", iconColor: "text-success", }, + rate_limit: { + icon: IconAlertTriangle, + label: "Rate Limited", + badgeVariant: "warning", + iconBg: "bg-warning/10", + iconColor: "text-warning", + }, system: { icon: IconInfoCircle, label: "System", diff --git a/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx b/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx index 757006f55..e497cab48 100644 --- a/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx +++ b/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx @@ -17,6 +17,8 @@ import { IconGitPullRequest, IconDotsVertical, IconAlertCircle, + IconAlertTriangle, + IconClock, } from "@tabler/icons-react"; import dayjs from "@conductor/shared/dates"; import { useQuery } from "convex/react"; @@ -36,6 +38,7 @@ interface QuickTaskCardProps { createdAt: number; createdBy?: Id<"users">; branchName?: string; + scheduledRetryAt?: number; onClick?: () => void; isSelecting?: boolean; isSelected?: boolean; @@ -50,6 +53,7 @@ export function QuickTaskCard({ createdAt, createdBy, branchName, + scheduledRetryAt, onClick, isSelecting, isSelected, @@ -58,12 +62,18 @@ export function QuickTaskCard({ const { fullName } = useRepo(); const runs = useQuery(api.agentRuns.listByTask, { taskId: id }); const latestPrUrl = runs?.find((r) => r.prUrl)?.prUrl; - const hasError = runs?.[0]?.status === "error"; + const latestRun = runs?.[0]; + const hasError = latestRun?.status === "error"; + const isRateLimited = hasError && latestRun?.errorType === "rate_limit"; return (
@@ -153,10 +167,22 @@ export function QuickTaskCard({
{createdBy && } - {hasError && ( - - - Failed + {hasError && + (isRateLimited ? ( + + + Rate Limited + + ) : ( + + + Failed + + ))} + {scheduledRetryAt && ( + + + Retrying {dayjs(scheduledRetryAt).fromNow()} )}
diff --git a/apps/web/lib/components/quick-tasks/QuickTasksKanbanBoard.tsx b/apps/web/lib/components/quick-tasks/QuickTasksKanbanBoard.tsx index 034a9b36c..bdc8225f5 100644 --- a/apps/web/lib/components/quick-tasks/QuickTasksKanbanBoard.tsx +++ b/apps/web/lib/components/quick-tasks/QuickTasksKanbanBoard.tsx @@ -130,6 +130,7 @@ export function QuickTasksKanbanBoard({ status={task.status} createdAt={task.createdAt} createdBy={task.createdBy} + scheduledRetryAt={task.scheduledRetryAt} isSelecting={isSelecting} isSelected={selectedIds.has(task._id)} onToggleSelect={() => onToggleSelect(task._id)} @@ -143,6 +144,7 @@ export function QuickTasksKanbanBoard({ status={task.status} createdAt={task.createdAt} createdBy={task.createdBy} + scheduledRetryAt={task.scheduledRetryAt} isSelecting={isSelecting} isSelected={selectedIds.has(task._id)} /> diff --git a/apps/web/lib/components/quick-tasks/QuickTasksListView.tsx b/apps/web/lib/components/quick-tasks/QuickTasksListView.tsx index b22187075..67c24f623 100644 --- a/apps/web/lib/components/quick-tasks/QuickTasksListView.tsx +++ b/apps/web/lib/components/quick-tasks/QuickTasksListView.tsx @@ -262,6 +262,7 @@ export function QuickTasksListView({ status={task.status} createdAt={task.createdAt} createdBy={task.createdBy} + scheduledRetryAt={task.scheduledRetryAt} onClick={() => { if (isSelecting) { onToggleSelect(task._id); diff --git a/apps/web/lib/components/tasks/TaskDetailModal.tsx b/apps/web/lib/components/tasks/TaskDetailModal.tsx index 86010a9e5..716b79862 100644 --- a/apps/web/lib/components/tasks/TaskDetailModal.tsx +++ b/apps/web/lib/components/tasks/TaskDetailModal.tsx @@ -57,6 +57,7 @@ import { IconFolder, IconTags, IconGitBranch, + IconClock, } from "@tabler/icons-react"; import { FormEvent, useEffect, useRef, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; @@ -107,6 +108,7 @@ export function TaskDetailModal({ const updateTask = useMutation(api.agentTasks.update); const updateStatus = useMutation(api.agentTasks.updateStatus); const deleteTask = useMutation(api.agentTasks.deleteCascade); + const scheduleRetry = useMutation(api.agentTasks.scheduleRetry); const comments = useQuery(api.taskComments.listByTask, { taskId }); const createComment = useMutation(api.taskComments.create); const removeComment = useMutation(api.taskComments.remove); @@ -123,6 +125,8 @@ export function TaskDetailModal({ const [commentText, setCommentText] = useState(""); const [requestChangesPanel, setRequestChangesPanel] = useState(false); const [tagsInput, setTagsInput] = useState(""); + const [showScheduleDialog, setShowScheduleDialog] = useState(false); + const [isScheduling, setIsScheduling] = useState(false); const commentsEndRef = useRef(null); useEffect(() => { @@ -167,6 +171,10 @@ export function TaskDetailModal({ }; const latestPrUrl = runs?.find((r) => r.prUrl)?.prUrl; + const latestRun = runs?.[0]; + const isRateLimited = + latestRun?.status === "error" && latestRun?.errorType === "rate_limit"; + const limitResetAt = isRateLimited ? latestRun?.limitResetAt : undefined; const status = task?.status; const showProofSection = status !== "todo" && status !== "in_progress"; const projectOptions = projects ?? []; @@ -337,6 +345,36 @@ export function TaskDetailModal({
)} + {isRateLimited && ( +
+
+ + Usage limit reached +
+

+ {limitResetAt + ? `Resets ${dayjs(limitResetAt).fromNow()} (${dayjs(limitResetAt).format("h:mm A")})` + : "Reset time unknown"} +

+ {task?.scheduledRetryAt ? ( +

+ + Retry scheduled {dayjs(task.scheduledRetryAt).fromNow()} +

+ ) : ( + + )} +
+ )} + {runs && runs.length > 0 && (

@@ -357,13 +395,18 @@ export function TaskDetailModal({ run.status === "success" ? "success" : run.status === "error" - ? "destructive" + ? run.errorType === "rate_limit" + ? "warning" + : "destructive" : run.status === "running" ? "warning" : "outline" } > - {run.status} + {run.status === "error" && + run.errorType === "rate_limit" + ? "rate limited" + : run.status} {run.startedAt @@ -1069,6 +1112,57 @@ export function TaskDetailModal({ + + + + + + + Schedule Retry + + +
+

+ All accounts have reached their usage limits. + {limitResetAt + ? ` The earliest reset is ${dayjs(limitResetAt).fromNow()} (${dayjs(limitResetAt).format("h:mm A")}).` + : ""} +

+

+ Schedule this task to automatically retry when limits reset. +

+
+ + + + +
+
); } diff --git a/packages/backend/convex/agentRuns.ts b/packages/backend/convex/agentRuns.ts index 7eb0cdc51..661d794c8 100644 --- a/packages/backend/convex/agentRuns.ts +++ b/packages/backend/convex/agentRuns.ts @@ -1,7 +1,11 @@ import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import type { Id } from "./_generated/dataModel"; -import { runStatusValidator, logLevelValidator } from "./validators"; +import { + runStatusValidator, + logLevelValidator, + errorTypeValidator, +} from "./validators"; import { createNotification } from "./notifications"; const logEntryValidator = v.object({ @@ -21,6 +25,8 @@ const agentRunValidator = v.object({ resultSummary: v.optional(v.string()), prUrl: v.optional(v.string()), error: v.optional(v.string()), + errorType: v.optional(errorTypeValidator), + limitResetAt: v.optional(v.number()), }); export const get = query({ diff --git a/packages/backend/convex/agentTasks.ts b/packages/backend/convex/agentTasks.ts index f0a554985..3fd6e8796 100644 --- a/packages/backend/convex/agentTasks.ts +++ b/packages/backend/convex/agentTasks.ts @@ -1,6 +1,7 @@ -import { mutation, query } from "./_generated/server"; +import { mutation, query, internalMutation } from "./_generated/server"; import { v } from "convex/values"; import { Id } from "./_generated/dataModel"; +import { internal } from "./_generated/api"; import { getCurrentUserId } from "./auth"; import { taskStatusValidator, claudeModelValidator } from "./validators"; import { createNotification } from "./notifications"; @@ -41,6 +42,7 @@ const agentTaskValidator = v.object({ assignedTo: v.optional(v.id("users")), model: v.optional(claudeModelValidator), activeWorkflowId: v.optional(v.string()), + scheduledRetryAt: v.optional(v.number()), }); export const listByBoard = query({ @@ -831,3 +833,51 @@ export const deleteCascade = mutation({ return null; }, }); + +export const scheduleRetry = mutation({ + args: { + taskId: v.id("agentTasks"), + retryAt: v.number(), + }, + returns: v.null(), + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + throw new Error("Not authenticated"); + } + const task = await ctx.db.get(args.taskId); + if (!task) { + throw new Error("Task not found"); + } + const board = await ctx.db.get(task.boardId); + if (!board || board.ownerId !== identity.subject) { + throw new Error("Task not found"); + } + + await ctx.db.patch(args.taskId, { + scheduledRetryAt: args.retryAt, + updatedAt: Date.now(), + }); + + ctx.scheduler.runAt(args.retryAt, internal.agentTasks.clearScheduledRetry, { + taskId: args.taskId, + }); + + return null; + }, +}); + +export const clearScheduledRetry = internalMutation({ + args: { taskId: v.id("agentTasks") }, + returns: v.null(), + handler: async (ctx, args) => { + const task = await ctx.db.get(args.taskId); + if (task && task.scheduledRetryAt) { + await ctx.db.patch(args.taskId, { + scheduledRetryAt: undefined, + updatedAt: Date.now(), + }); + } + return null; + }, +}); diff --git a/packages/backend/convex/designWorkflow.ts b/packages/backend/convex/designWorkflow.ts index 771b870e3..4a6f65959 100644 --- a/packages/backend/convex/designWorkflow.ts +++ b/packages/backend/convex/designWorkflow.ts @@ -352,6 +352,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/docInterviewWorkflow.ts b/packages/backend/convex/docInterviewWorkflow.ts index 4c8b0a024..0b601467b 100644 --- a/packages/backend/convex/docInterviewWorkflow.ts +++ b/packages/backend/convex/docInterviewWorkflow.ts @@ -300,6 +300,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/docPrdWorkflow.ts b/packages/backend/convex/docPrdWorkflow.ts index d1e8ebd2f..a4f903e91 100644 --- a/packages/backend/convex/docPrdWorkflow.ts +++ b/packages/backend/convex/docPrdWorkflow.ts @@ -220,6 +220,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/evaluationWorkflow.ts b/packages/backend/convex/evaluationWorkflow.ts index 701e5cc9e..85c533fe6 100644 --- a/packages/backend/convex/evaluationWorkflow.ts +++ b/packages/backend/convex/evaluationWorkflow.ts @@ -228,6 +228,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/projectInterviewWorkflow.ts b/packages/backend/convex/projectInterviewWorkflow.ts index 2d506be23..77f5378fa 100644 --- a/packages/backend/convex/projectInterviewWorkflow.ts +++ b/packages/backend/convex/projectInterviewWorkflow.ts @@ -300,6 +300,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/researchQueryWorkflow.ts b/packages/backend/convex/researchQueryWorkflow.ts index 751eedd7b..b2764e475 100644 --- a/packages/backend/convex/researchQueryWorkflow.ts +++ b/packages/backend/convex/researchQueryWorkflow.ts @@ -434,6 +434,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/sessionAudits.ts b/packages/backend/convex/sessionAudits.ts index ecdf6a35d..b7d251b25 100644 --- a/packages/backend/convex/sessionAudits.ts +++ b/packages/backend/convex/sessionAudits.ts @@ -82,6 +82,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/sessionWorkflow.ts b/packages/backend/convex/sessionWorkflow.ts index 3b29b5ea9..ff581f0f3 100644 --- a/packages/backend/convex/sessionWorkflow.ts +++ b/packages/backend/convex/sessionWorkflow.ts @@ -15,6 +15,9 @@ const sessionCompleteEvent = defineEvent({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }), }); @@ -235,7 +238,46 @@ export const sessionExecuteWorkflow = workflow.define({ } // Step 5: Wait for sandbox callback - const result = await step.awaitEvent(sessionCompleteEvent); + let result = await step.awaitEvent(sessionCompleteEvent); + + // Step 5b: Handle rate limit — try auto-switching to another account + if (!result.success && result.errorType === "rate_limit") { + if (result.accountKey) { + await step.runMutation(internal.aiAccounts.markAccountLimited, { + accountKey: result.accountKey, + limitResetAt: result.limitResetAt + ? new Date(result.limitResetAt).getTime() + : undefined, + }); + } + + await step.runMutation(internal.aiAccounts.clearExpiredLimits, {}); + const nextAccount = await step.runQuery( + internal.aiAccounts.getAvailableAccountKey, + {}, + ); + + if (nextAccount) { + await step.runAction(internal.daytona.setupAndExecute, { + entityId: args.sessionId, + existingSandboxId: sandboxId, + githubToken: args.githubToken, + repoOwner: data.repoOwner, + repoName: data.repoName, + prompt: data.prompt, + convexToken: args.convexToken, + completionMutation: "sessionWorkflow:handleCompletion", + entityIdField: "sessionId", + model: data.model, + allowedTools: data.allowedTools, + branchName: data.branchName, + repoId: data.repoId, + oauthAccountKey: nextAccount, + }); + + result = await step.awaitEvent(sessionCompleteEvent); + } + } // Step 6: Post-completion mode-specific operations let fileDiffs: string | undefined; @@ -481,6 +523,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { @@ -499,6 +544,9 @@ export const handleCompletion = mutation({ result: args.result, error: args.error, activityLog: args.activityLog, + errorType: args.errorType, + limitResetAt: args.limitResetAt, + accountKey: args.accountKey, }, }); diff --git a/packages/backend/convex/summarizeWorkflow.ts b/packages/backend/convex/summarizeWorkflow.ts index 8dfc63968..f85b4b82d 100644 --- a/packages/backend/convex/summarizeWorkflow.ts +++ b/packages/backend/convex/summarizeWorkflow.ts @@ -154,6 +154,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { diff --git a/packages/backend/convex/taskWorkflow.ts b/packages/backend/convex/taskWorkflow.ts index e1984e403..2c5e3de42 100644 --- a/packages/backend/convex/taskWorkflow.ts +++ b/packages/backend/convex/taskWorkflow.ts @@ -17,6 +17,9 @@ const taskCompleteEvent = defineEvent({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }), }); @@ -158,7 +161,47 @@ export const taskExecutionWorkflow = workflow.define({ } // Step 5: Wait for sandbox callback - const result = await step.awaitEvent(taskCompleteEvent); + let result = await step.awaitEvent(taskCompleteEvent); + + // Step 5b: Handle rate limit — try auto-switching to another account + if (!result.success && result.errorType === "rate_limit") { + if (result.accountKey) { + await step.runMutation(internal.aiAccounts.markAccountLimited, { + accountKey: result.accountKey, + limitResetAt: result.limitResetAt + ? new Date(result.limitResetAt).getTime() + : undefined, + }); + } + + await step.runMutation(internal.aiAccounts.clearExpiredLimits, {}); + const nextAccount = await step.runQuery( + internal.aiAccounts.getAvailableAccountKey, + {}, + ); + + if (nextAccount) { + await step.runAction(internal.daytona.setupAndExecute, { + entityId: String(args.taskId), + existingSandboxId: sandboxId, + githubToken: args.githubToken, + repoOwner: data.repoOwner, + repoName: data.repoName, + prompt: data.prompt, + convexToken: args.convexToken, + completionMutation: "taskWorkflow:handleCompletion", + entityIdField: "taskId", + model: args.model ?? "sonnet", + allowedTools: "Read,Write,Edit,Bash,Glob,Grep", + branchName: data.branchName, + baseBranch: args.baseBranch, + repoId: args.repoId, + oauthAccountKey: nextAccount, + }); + + result = await step.awaitEvent(taskCompleteEvent); + } + } // Step 6: Create PR if first task on branch let prUrl: string | null = null; @@ -185,6 +228,14 @@ export const taskExecutionWorkflow = workflow.define({ error: result.error, prUrl, hasSubtasks: data.hasSubtasks, + errorType: + !result.success && result.errorType === "rate_limit" + ? "rate_limit" + : undefined, + limitResetAt: + !result.success && result.limitResetAt + ? new Date(result.limitResetAt).getTime() + : undefined, }); // Step 8: Run audit (non-fatal, fire-and-forget via Daytona) @@ -338,6 +389,10 @@ export const completeRun = internalMutation({ error: v.union(v.string(), v.null()), prUrl: v.union(v.string(), v.null()), hasSubtasks: v.boolean(), + errorType: v.optional( + v.union(v.literal("rate_limit"), v.literal("generic")), + ), + limitResetAt: v.optional(v.number()), }, returns: v.null(), handler: async (ctx, args) => { @@ -354,6 +409,8 @@ export const completeRun = internalMutation({ : undefined, prUrl: args.prUrl ?? undefined, error: args.success ? undefined : (args.error ?? "Unknown error"), + errorType: args.errorType, + limitResetAt: args.limitResetAt, }); // Update task status @@ -399,7 +456,12 @@ export const completeRun = internalMutation({ // Create notifications if (task) { - const statusText = args.success ? "succeeded" : "failed"; + const isRateLimit = args.errorType === "rate_limit"; + const statusText = args.success + ? "succeeded" + : isRateLimit + ? "hit usage limit" + : "failed"; const notifyUsers = new Set( [task.createdBy, task.assignedTo].filter( (id): id is Id<"users"> => id !== undefined, @@ -408,7 +470,7 @@ export const completeRun = internalMutation({ for (const userId of notifyUsers) { await createNotification(ctx, { userId, - type: "run_completed", + type: isRateLimit ? "rate_limit" : "run_completed", title: `Run ${statusText} for "${task.title}"`, repoId: task.repoId, projectId: task.projectId, @@ -535,6 +597,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => { @@ -552,6 +617,9 @@ export const handleCompletion = mutation({ result: args.result, error: args.error, activityLog: args.activityLog, + errorType: args.errorType, + limitResetAt: args.limitResetAt, + accountKey: args.accountKey, }, }); diff --git a/packages/backend/convex/testGenWorkflow.ts b/packages/backend/convex/testGenWorkflow.ts index 08356fe74..21fa9e205 100644 --- a/packages/backend/convex/testGenWorkflow.ts +++ b/packages/backend/convex/testGenWorkflow.ts @@ -301,6 +301,9 @@ export const handleCompletion = mutation({ result: v.union(v.string(), v.null()), error: v.union(v.string(), v.null()), activityLog: v.union(v.string(), v.null()), + errorType: v.optional(v.union(v.literal("rate_limit"), v.null())), + limitResetAt: v.optional(v.union(v.string(), v.null())), + accountKey: v.optional(v.union(v.string(), v.null())), }, returns: v.null(), handler: async (ctx, args) => {