feat(backend): queue-time runtime/profile validation; job.runtime authoritative in worker
This commit is contained in:
@@ -4,6 +4,8 @@ import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getOwnedSpoon, getRequiredUserId, optionalText } from './model';
|
||||
import type { AgentRuntimeName } from './runtimeSupport';
|
||||
import { runtimesForProfile } from './runtimeSupport';
|
||||
import { assertWorkerToken } from './workerAuth';
|
||||
|
||||
const jobStatus = v.union(
|
||||
@@ -19,7 +21,11 @@ const jobStatus = v.union(
|
||||
v.literal('timed_out'),
|
||||
);
|
||||
|
||||
const runtime = v.literal('opencode');
|
||||
const runtime = v.union(
|
||||
v.literal('codex'),
|
||||
v.literal('opencode'),
|
||||
v.literal('claude'),
|
||||
);
|
||||
|
||||
const jobType = v.union(
|
||||
v.literal('user_change'),
|
||||
@@ -141,7 +147,6 @@ const maintenanceRisk = v.union(
|
||||
|
||||
const defaultAgentSettings = {
|
||||
enabled: true,
|
||||
runtime: 'opencode' as const,
|
||||
branchPrefix: 'spoon/agent',
|
||||
agentModel: '',
|
||||
reasoningEffort: 'medium' as const,
|
||||
@@ -440,7 +445,7 @@ const insertJob = async (
|
||||
| 'conflict_resolution';
|
||||
baseBranch?: string;
|
||||
requestedBranchName?: string;
|
||||
requestedRuntime?: 'opencode';
|
||||
requestedRuntime?: AgentRuntimeName;
|
||||
materializeEnvFile?: boolean;
|
||||
requestedEnvFilePath?: string;
|
||||
requestedProfileId?: Id<'aiProviderProfiles'>;
|
||||
@@ -464,7 +469,23 @@ const insertJob = async (
|
||||
const now = Date.now();
|
||||
const resolvedBaseBranch =
|
||||
optionalText(baseBranch) ?? settings.defaultBaseBranch;
|
||||
const jobRuntime = requestedRuntime ?? 'opencode';
|
||||
const supported = runtimesForProfile(profile);
|
||||
// An explicit request is authoritative and must be supported. A persisted
|
||||
// spoon runtime (seeded to 'opencode' for every Spoon) is only honored when
|
||||
// the profile can actually drive it; otherwise fall back to the profile's
|
||||
// primary runtime so ChatGPT-login profiles still resolve to codex.
|
||||
const settingsRuntime = settings.runtime as AgentRuntimeName | undefined;
|
||||
const primaryRuntime = supported[0] ?? 'opencode';
|
||||
const resolvedRuntime =
|
||||
requestedRuntime ??
|
||||
(settingsRuntime && supported.includes(settingsRuntime)
|
||||
? settingsRuntime
|
||||
: primaryRuntime);
|
||||
if (!supported.includes(resolvedRuntime)) {
|
||||
throw new ConvexError(
|
||||
`Provider "${profile.name}" cannot run the "${resolvedRuntime}" runtime. Supported: ${supported.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
const shouldMaterializeEnvFile =
|
||||
materializeEnvFile ?? settings.materializeEnvFileByDefault;
|
||||
const envFilePath =
|
||||
@@ -488,7 +509,7 @@ const insertJob = async (
|
||||
jobType: requestedJobType,
|
||||
status: 'queued',
|
||||
prompt,
|
||||
runtime: jobRuntime,
|
||||
runtime: resolvedRuntime,
|
||||
workspaceStatus: 'not_started',
|
||||
baseBranch: resolvedBaseBranch,
|
||||
workBranch,
|
||||
@@ -596,6 +617,7 @@ export const createForThread = mutation({
|
||||
jobType,
|
||||
baseBranch: v.optional(v.string()),
|
||||
requestedBranchName: v.optional(v.string()),
|
||||
runtime: v.optional(runtime),
|
||||
materializeEnvFile: v.optional(v.boolean()),
|
||||
envFilePath: v.optional(v.string()),
|
||||
aiProviderProfileId: v.optional(v.id('aiProviderProfiles')),
|
||||
@@ -649,6 +671,7 @@ export const createForThread = mutation({
|
||||
requestedJobType: args.jobType,
|
||||
baseBranch: args.baseBranch,
|
||||
requestedBranchName: args.requestedBranchName,
|
||||
requestedRuntime: args.runtime,
|
||||
materializeEnvFile: args.materializeEnvFile,
|
||||
requestedEnvFilePath: args.envFilePath,
|
||||
requestedProfileId: args.aiProviderProfileId,
|
||||
@@ -664,6 +687,7 @@ export const createForThreadInternal = internalMutation({
|
||||
jobType,
|
||||
baseBranch: v.optional(v.string()),
|
||||
requestedBranchName: v.optional(v.string()),
|
||||
runtime: v.optional(runtime),
|
||||
materializeEnvFile: v.optional(v.boolean()),
|
||||
envFilePath: v.optional(v.string()),
|
||||
aiProviderProfileId: v.optional(v.id('aiProviderProfiles')),
|
||||
@@ -719,6 +743,7 @@ export const createForThreadInternal = internalMutation({
|
||||
requestedJobType: args.jobType,
|
||||
baseBranch: args.baseBranch,
|
||||
requestedBranchName: args.requestedBranchName,
|
||||
requestedRuntime: args.runtime,
|
||||
materializeEnvFile: args.materializeEnvFile,
|
||||
requestedEnvFilePath: args.envFilePath,
|
||||
requestedProfileId: args.aiProviderProfileId,
|
||||
@@ -1140,7 +1165,7 @@ export const claimNextInternal = internalMutation({
|
||||
await failJobClaim(ctx, job, 'Agent jobs are disabled for this Spoon.');
|
||||
return null;
|
||||
}
|
||||
if ((job.runtime ?? 'opencode') !== 'opencode') {
|
||||
if (job.runtime === 'openai_direct') {
|
||||
await failJobClaim(
|
||||
ctx,
|
||||
job,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getRequiredUserId, optionalText } from './model';
|
||||
import { runtimesForProfile } from './runtimeSupport';
|
||||
|
||||
type AiProviderProfileWithDefault = Doc<'aiProviderProfiles'> & {
|
||||
isDefault?: boolean;
|
||||
@@ -59,6 +60,7 @@ const publicProfile = (
|
||||
reasoningEffort: profile.reasoningEffort,
|
||||
enabled: profile.enabled,
|
||||
configured: isConfigured(profile),
|
||||
supportedRuntimes: runtimesForProfile(profile),
|
||||
isDefault: profile._id === defaultProfileId,
|
||||
createdAt: profile.createdAt,
|
||||
updatedAt: profile.updatedAt,
|
||||
|
||||
@@ -245,6 +245,9 @@ export const createUserThread = mutation({
|
||||
prompt: v.string(),
|
||||
baseBranch: v.optional(v.string()),
|
||||
requestedBranchName: v.optional(v.string()),
|
||||
runtime: v.optional(
|
||||
v.union(v.literal('codex'), v.literal('opencode'), v.literal('claude')),
|
||||
),
|
||||
materializeEnvFile: v.optional(v.boolean()),
|
||||
envFilePath: v.optional(v.string()),
|
||||
aiProviderProfileId: v.optional(v.id('aiProviderProfiles')),
|
||||
@@ -284,6 +287,7 @@ export const createUserThread = mutation({
|
||||
jobType: 'user_change',
|
||||
baseBranch: args.baseBranch,
|
||||
requestedBranchName: args.requestedBranchName,
|
||||
runtime: args.runtime,
|
||||
materializeEnvFile: args.materializeEnvFile,
|
||||
envFilePath: args.envFilePath,
|
||||
aiProviderProfileId: args.aiProviderProfileId,
|
||||
|
||||
Reference in New Issue
Block a user