Try to fix workers and workspace
This commit is contained in:
+115
-14
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user