fix(agent-jobs): recover failed claim decryption
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -58,6 +58,24 @@ export const claimNextForWorker = action({
|
||||
if (!claimed) return null;
|
||||
if (!claimed.spoon || !claimed.aiProviderProfile) return null;
|
||||
const profile = claimed.aiProviderProfile;
|
||||
let profileSecret: string | undefined;
|
||||
let secrets: { name: string; value: string }[];
|
||||
try {
|
||||
profileSecret = profile.encryptedSecret
|
||||
? decryptSecret(profile.encryptedSecret)
|
||||
: undefined;
|
||||
secrets = claimed.secrets.map((secret: Doc<'spoonSecrets'>) => ({
|
||||
name: secret.name,
|
||||
value: decryptSecret(secret.encryptedValue),
|
||||
}));
|
||||
} catch {
|
||||
await ctx.runMutation(internal.agentJobs.failClaimInternal, {
|
||||
jobId: claimed.job._id,
|
||||
workerId: args.workerId,
|
||||
reason: 'Stored AI credentials or Spoon secrets could not be decrypted.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
job: claimed.job,
|
||||
spoon: claimed.spoon,
|
||||
@@ -70,9 +88,7 @@ export const claimNextForWorker = action({
|
||||
name: profile.name,
|
||||
provider: profile.provider,
|
||||
authType: profile.authType,
|
||||
secret: profile.encryptedSecret
|
||||
? decryptSecret(profile.encryptedSecret)
|
||||
: undefined,
|
||||
secret: profileSecret,
|
||||
baseUrl: profile.baseUrl,
|
||||
model: profile.defaultModel,
|
||||
reasoningEffort: profile.reasoningEffort,
|
||||
@@ -84,10 +100,7 @@ export const claimNextForWorker = action({
|
||||
claimed.spoon.githubInstallationId ??
|
||||
process.env.GITHUB_APP_INSTALLATION_ID,
|
||||
},
|
||||
secrets: claimed.secrets.map((secret: Doc<'spoonSecrets'>) => ({
|
||||
name: secret.name,
|
||||
value: decryptSecret(secret.encryptedValue),
|
||||
})),
|
||||
secrets,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user