fix(agent-jobs): validate claims before claiming

This commit is contained in:
Gabriel Brown
2026-07-10 17:26:17 -04:00
parent 926008fd3a
commit cf45404b7d
3 changed files with 281 additions and 17 deletions
+60 -2
View File
@@ -1074,6 +1074,65 @@ export const claimNextInternal = internalMutation({
secrets.push(secret);
}
}
const ownedProfile =
aiProviderProfile?.ownerId === job.ownerId && aiProviderProfile.enabled
? aiProviderProfile
: null;
const failClaim = async (reason: string) => {
const failedAt = Date.now();
await ctx.db.patch(job._id, {
status: 'failed',
error: reason,
completedAt: failedAt,
updatedAt: failedAt,
});
await ctx.db.patch(job.agentRequestId, {
status: 'failed',
updatedAt: failedAt,
});
if (job.threadId) {
await ctx.db.patch(job.threadId, {
status: 'failed',
updatedAt: failedAt,
resolvedAt: failedAt,
});
}
await ctx.db.insert('agentJobEvents', {
jobId: job._id,
spoonId: job.spoonId,
ownerId: job.ownerId,
level: 'error',
phase: 'queued',
message: `Job could not be claimed: ${reason}`,
createdAt: failedAt,
});
};
if (!spoon) {
await failClaim('The Spoon for this job no longer exists.');
return null;
}
if (!ownedProfile) {
await failClaim(
'AI is not configured for this user. Add an AI provider in settings.',
);
return null;
}
if (ownedProfile.authType !== 'none' && !ownedProfile.encryptedSecret) {
await failClaim('Selected AI provider is missing credentials.');
return null;
}
if (agentSettings && !agentSettings.enabled) {
await failClaim('Agent jobs are disabled for this Spoon.');
return null;
}
if ((job.runtime ?? 'opencode') !== 'opencode') {
await failClaim('Legacy OpenAI direct jobs are no longer supported.');
return null;
}
if (secrets.length !== job.selectedSecretIds.length) {
await failClaim('Selected secrets must belong to this Spoon.');
return null;
}
const now = Date.now();
await ctx.db.patch(job._id, {
status: 'claimed',
@@ -1098,8 +1157,7 @@ export const claimNextInternal = internalMutation({
job: { ...job, status: 'claimed' as const, claimedBy: workerId },
spoon,
aiSettings,
aiProviderProfile:
aiProviderProfile?.ownerId === job.ownerId ? aiProviderProfile : null,
aiProviderProfile: ownedProfile,
agentSettings,
secrets,
};
+2 -15
View File
@@ -1,6 +1,6 @@
'use node';
import { ConvexError, v } from 'convex/values';
import { v } from 'convex/values';
import type { Doc } from './_generated/dataModel';
import { internal } from './_generated/api';
@@ -56,20 +56,7 @@ export const claimNextForWorker = action({
},
)) as ClaimedJob | null;
if (!claimed) return null;
if (!claimed.spoon) {
throw new ConvexError('Claimed job points at a missing Spoon.');
}
if (!claimed.aiProviderProfile) {
throw new ConvexError(
'AI is not configured for this user. Add an AI provider in settings.',
);
}
if (
claimed.aiProviderProfile.authType !== 'none' &&
!claimed.aiProviderProfile.encryptedSecret
) {
throw new ConvexError('Selected AI provider is missing credentials.');
}
if (!claimed.spoon || !claimed.aiProviderProfile) return null;
const profile = claimed.aiProviderProfile;
return {
job: claimed.job,