From 4680954224cc15f5019e9a2fe6a9a60c2739cf12 Mon Sep 17 00:00:00 2001 From: Vedant <48868398+vedantb2@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:22:21 +0000 Subject: [PATCH] feat: rate limit detection and auto account rotation for Claude OAuth Add errorType, limitResetAt, and accountKey fields to all handleCompletion mutations so the shared callback script can report rate limits. Task and session workflows auto-retry with the next available OAuth account when rate limited. Also includes quick-task and notification UI improvements. --- .../notifications/notification-config.tsx | 8 ++ .../components/quick-tasks/QuickTaskCard.tsx | 40 ++++++-- .../quick-tasks/QuickTasksKanbanBoard.tsx | 2 + .../quick-tasks/QuickTasksListView.tsx | 1 + .../lib/components/tasks/TaskDetailModal.tsx | 98 ++++++++++++++++++- packages/backend/convex/agentRuns.ts | 8 +- packages/backend/convex/agentTasks.ts | 52 +++++++++- packages/backend/convex/designWorkflow.ts | 3 + .../backend/convex/docInterviewWorkflow.ts | 3 + packages/backend/convex/docPrdWorkflow.ts | 3 + packages/backend/convex/evaluationWorkflow.ts | 3 + .../convex/projectInterviewWorkflow.ts | 3 + .../backend/convex/researchQueryWorkflow.ts | 3 + packages/backend/convex/sessionAudits.ts | 3 + packages/backend/convex/sessionWorkflow.ts | 50 +++++++++- packages/backend/convex/summarizeWorkflow.ts | 3 + packages/backend/convex/taskWorkflow.ts | 74 +++++++++++++- packages/backend/convex/testGenWorkflow.ts | 3 + 18 files changed, 345 insertions(+), 15 deletions(-) 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) => {