Try to fix workers and workspace
Build and Push Spoon Images / quality (push) Successful in 1m40s
Build and Push Spoon Images / build-images (push) Successful in 7m0s

This commit is contained in:
Gabriel Brown
2026-06-22 23:17:27 -04:00
parent f33f76d874
commit 930fbf5965
11 changed files with 208 additions and 48 deletions
+115 -14
View File
@@ -94,6 +94,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const client = new ConvexHttpClient(env.convexUrl);
const activeWorkspaces = new Map<string, ActiveWorkspace>();
const jobContainerWorkspace = '/workspace';
const appendEvent = async (
jobId: Id<'agentJobs'>,
@@ -239,7 +240,50 @@ const recordWorkspaceChange = async (args: {
const commandToShell = (command: string) => ['bash', '-lc', command];
const providerEnvironment = (claim: Claim): Record<string, string> => {
const isCodexLoginProfile = (claim: Claim) =>
claim.aiProviderProfile?.provider === 'opencode_openai_login' ||
claim.aiProviderProfile?.authType === 'opencode_auth_json';
const collectJsonStringValues = (value?: string): string[] => {
if (!value) return [];
try {
const parsed = JSON.parse(value) as unknown;
const values: string[] = [];
const visit = (item: unknown) => {
if (typeof item === 'string') {
if (item.length >= 12) values.push(item);
return;
}
if (Array.isArray(item)) {
item.forEach(visit);
return;
}
if (item && typeof item === 'object') {
Object.values(item).forEach(visit);
}
};
visit(parsed);
return values;
} catch {
return [];
}
};
const providerEnvironment = (
claim: Claim,
workspaceRoot?: string,
): Record<string, string> => {
if (isCodexLoginProfile(claim)) {
if (!workspaceRoot) {
throw new Error('Codex auth profiles require a prepared workspace.');
}
return {
CODEX_HOME: path.join(workspaceRoot, '.codex'),
HOME: workspaceRoot,
XDG_DATA_HOME: path.join(workspaceRoot, '.local', 'share'),
XDG_CONFIG_HOME: path.join(workspaceRoot, '.config'),
};
}
const profile = claim.aiProviderProfile;
const secret = profile?.secret ?? claim.openai.apiKey;
if (!secret) {
@@ -268,9 +312,7 @@ const providerEnvironment = (claim: Claim): Record<string, string> => {
) {
return { OPENAI_API_KEY: secret, ...baseUrl };
}
throw new Error(
'OpenCode login profiles are saved but need auth-file injection before execution.',
);
throw new Error('Unsupported AI provider profile.');
};
const opencodeModel = (claim: Claim) => {
@@ -288,6 +330,63 @@ const opencodeModel = (claim: Claim) => {
return `${profile.provider}/${model}`;
};
const codexModel = (claim: Claim) => {
const model = claim.aiProviderProfile?.model ?? claim.openai.model;
return model.includes('/') ? model.split('/').at(-1) ?? model : model;
};
const writeJsonFile = async (filePath: string, content: string) => {
let normalized = content.trim();
try {
normalized = `${JSON.stringify(JSON.parse(normalized), null, 2)}\n`;
} catch {
throw new Error('Codex auth JSON is not valid JSON.');
}
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, normalized, { mode: 0o600 });
};
const prepareCodexAuth = async (workspace: ActiveWorkspace) => {
const secret = workspace.claim.aiProviderProfile?.secret;
if (!secret) {
throw new Error('Codex auth profile is missing auth.json contents.');
}
const codexAuthPath = path.join(workspace.workdir, '.codex', 'auth.json');
await writeJsonFile(codexAuthPath, secret);
// Also seed OpenCode's auth location with the saved JSON for forward
// compatibility if this profile later runs through OpenCode directly.
const openCodeAuthPath = path.join(
workspace.workdir,
'.local',
'share',
'opencode',
'auth.json',
);
await writeJsonFile(openCodeAuthPath, secret);
await appendEvent(
workspace.claim.job._id,
'info',
'clone',
'Prepared Codex auth JSON for the isolated workspace.',
);
};
const agentCommand = (claim: Claim, prompt: string) => {
if (isCodexLoginProfile(claim)) {
return commandToShell(
`codex exec --model ${quoteShell(codexModel(claim))} --sandbox workspace-write ${quoteShell(prompt)}`,
);
}
return commandToShell(
`opencode run --model ${quoteShell(opencodeModel(claim))} ${quoteShell(prompt)}`,
);
};
const agentFailurePrefix = (claim: Claim) =>
isCodexLoginProfile(claim) ? 'codex failed' : 'opencode failed';
const systemPromptForJob = (claim: Claim) => {
const base = [
`Spoon: ${claim.spoon.name}`,
@@ -605,6 +704,7 @@ const runClaim = async (claim: Claim) => {
const secretValues = [
claim.openai.apiKey ?? '',
claim.aiProviderProfile?.secret ?? '',
...collectJsonStringValues(claim.aiProviderProfile?.secret),
...claim.secrets.map((secret) => secret.value),
].filter(Boolean);
const redact = createRedactor(secretValues);
@@ -632,6 +732,9 @@ const runClaim = async (claim: Claim) => {
githubToken,
redact,
};
if (isCodexLoginProfile(claim)) {
await prepareCodexAuth(workspace);
}
await materializeEnvFile(workspace);
const detected = await detectPackageCommands(repoDir);
await addArtifact({
@@ -800,18 +903,19 @@ export const sendWorkspaceMessage = async (jobId: string, prompt: string) => {
if ((claim.job.runtime ?? 'opencode') !== 'opencode') {
throw new Error('Legacy OpenAI direct jobs are no longer supported.');
}
const model = opencodeModel(claim);
const aiEnv = providerEnvironment(claim);
const aiEnv = providerEnvironment(
claim,
env.runtime === 'docker' ? jobContainerWorkspace : workdir,
);
const secretEnv = Object.fromEntries(
claim.secrets.map((secret) => [secret.name, secret.value]),
);
const command = agentCommand(claim, prompt);
const result =
env.runtime === 'docker'
? await runInJobContainer({
workdir,
command: commandToShell(
`opencode run --model ${quoteShell(model)} ${quoteShell(prompt)}`,
),
command,
environment: {
...aiEnv,
...secretEnv,
@@ -821,10 +925,7 @@ export const sendWorkspaceMessage = async (jobId: string, prompt: string) => {
})
: await run(
'bash',
[
'-lc',
`opencode run --model ${quoteShell(model)} ${quoteShell(prompt)}`,
],
command.slice(1),
{
cwd: repoDir,
env: {
@@ -842,7 +943,7 @@ export const sendWorkspaceMessage = async (jobId: string, prompt: string) => {
content: truncate(result.output, 40_000),
});
if (result.exitCode !== 0) {
throw new Error(`opencode failed:\n${result.output}`);
throw new Error(`${agentFailurePrefix(claim)}:\n${result.output}`);
}
if (claim.job.jobType === 'maintenance_review') {
const decision = parseMaintenanceDecision(result.output);