fix(agent-jobs): recover failed claim decryption

This commit is contained in:
Gabriel Brown
2026-07-10 17:26:17 -04:00
parent cf45404b7d
commit 1e1435e656
3 changed files with 238 additions and 66 deletions
+72 -35
View File
@@ -294,6 +294,40 @@ const deleteWorkspaceRows = async (ctx: MutationCtx, job: Doc<'agentJobs'>) => {
await ctx.db.delete(job._id);
};
const failJobClaim = async (
ctx: MutationCtx,
job: Doc<'agentJobs'>,
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,
});
};
const getAgentSettings = async (ctx: MutationCtx, spoon: Doc<'spoons'>) => {
const settings = await ctx.db
.query('spoonAgentSettings')
@@ -1078,59 +1112,48 @@ export const claimNextInternal = internalMutation({
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.');
await failJobClaim(
ctx,
job,
'The Spoon for this job no longer exists.',
);
return null;
}
if (!ownedProfile) {
await failClaim(
await failJobClaim(
ctx,
job,
'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.');
await failJobClaim(
ctx,
job,
'Selected AI provider is missing credentials.',
);
return null;
}
if (agentSettings && !agentSettings.enabled) {
await failClaim('Agent jobs are disabled for this Spoon.');
await failJobClaim(ctx, job, 'Agent jobs are disabled for this Spoon.');
return null;
}
if ((job.runtime ?? 'opencode') !== 'opencode') {
await failClaim('Legacy OpenAI direct jobs are no longer supported.');
await failJobClaim(
ctx,
job,
'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.');
await failJobClaim(
ctx,
job,
'Selected secrets must belong to this Spoon.',
);
return null;
}
const now = Date.now();
@@ -1164,6 +1187,20 @@ export const claimNextInternal = internalMutation({
},
});
export const failClaimInternal = internalMutation({
args: {
jobId: v.id('agentJobs'),
workerId: v.string(),
reason: v.string(),
},
handler: async (ctx, { jobId, workerId, reason }) => {
const job = await ctx.db.get(jobId);
if (job?.status !== 'claimed' || job.claimedBy !== workerId) return null;
await failJobClaim(ctx, job, reason);
return null;
},
});
export const updateStatus = mutation({
args: {
workerToken: v.string(),