Clean up old stuff & fix ui errors
Build and Push Spoon Images / quality (push) Successful in 2m22s
Build and Push Spoon Images / build-images (push) Successful in 23m10s

This commit is contained in:
Gabriel Brown
2026-06-23 14:57:05 -04:00
parent d207b8b0b8
commit a6f7ea7f78
34 changed files with 1565 additions and 551 deletions
+108 -24
View File
@@ -16,6 +16,11 @@ import { api } from '@spoon/backend/convex/_generated/api.js';
import type { NormalizedAgentEvent } from './agent-events';
import { normalizeCodexJsonLine } from './agent-events';
import {
codexContainerRepo,
codexContainerWorkspace,
prepareCodexWorkspaceFiles,
} from './codex-runtime';
import { env } from './env';
import {
cloneRepository,
@@ -118,7 +123,6 @@ 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'>,
@@ -442,6 +446,10 @@ const prepareCodexAuth = async (workspace: ActiveWorkspace) => {
if (!secret) {
throw new Error('Codex auth profile is missing auth.json contents.');
}
await prepareCodexWorkspaceFiles({
workdir: workspace.workdir,
repoDir: workspace.repoDir,
});
const codexAuthPath = path.join(workspace.workdir, '.codex', 'auth.json');
await writeJsonFile(codexAuthPath, secret);
@@ -688,14 +696,18 @@ const runCodexTurn = async (args: {
? commandToShell(
`codex exec resume --json --model ${quoteShell(
codexModel(workspace.claim),
)} ${quoteShell(workspace.codexSessionId)} ${quoteShell(prompt)}`,
)} --dangerously-bypass-approvals-and-sandbox ${quoteShell(
workspace.codexSessionId,
)} ${quoteShell(prompt)}`,
)
: commandToShell(
`codex exec --json --model ${quoteShell(
codexModel(workspace.claim),
)} --sandbox workspace-write ${quoteShell(prompt)}`,
)} --dangerously-bypass-approvals-and-sandbox --cd ${quoteShell(
codexContainerRepo,
)} ${quoteShell(prompt)}`,
);
const aiEnv = providerEnvironment(workspace.claim, jobContainerWorkspace);
const aiEnv = providerEnvironment(workspace.claim, codexContainerWorkspace);
const secretEnv = Object.fromEntries(
workspace.claim.secrets.map((secret) => [secret.name, secret.value]),
);
@@ -958,6 +970,89 @@ const fileChangedType = async (repoDir: string, filePath: string) => {
return 'modified' as const;
};
const sensitiveWorkspacePath = (filePath: string) => {
const parts = filePath.split('/');
if (parts.includes('.git') || parts.includes('.codex')) return true;
const name = parts.at(-1) ?? filePath;
if (name === '.env') return true;
if (name.startsWith('.env.') && name !== '.env.example') return true;
return false;
};
const changedFilesFromStatus = async (
repoDir: string,
redact: (value: string) => string,
) => {
const status = await run('git', ['status', '--short'], {
cwd: repoDir,
redact,
timeoutMs: 60_000,
});
if (status.exitCode !== 0) return [];
return status.output
.split('\n')
.map((line) => {
if (line.length < 4) return null;
const code = line.slice(0, 2);
const rawPath = line.slice(3).trim();
if (!rawPath) return null;
const filePath = rawPath.includes(' -> ')
? rawPath.split(' -> ').at(-1)?.trim()
: rawPath;
if (!filePath || sensitiveWorkspacePath(filePath)) return null;
const changeType = code.includes('D')
? 'deleted'
: code.includes('R')
? 'renamed'
: code.includes('A') || code.includes('?')
? 'added'
: 'modified';
return {
path: filePath,
changeType,
};
})
.filter(
(
value,
): value is {
path: string;
changeType: 'added' | 'modified' | 'deleted' | 'renamed';
} => Boolean(value),
);
};
const recordChangedFiles = async (
workspace: ActiveWorkspace,
source: 'agent' | 'command',
diff: string,
) => {
const changes = await changedFilesFromStatus(
workspace.repoDir,
workspace.redact,
);
for (const change of changes) {
await recordWorkspaceChange({
jobId: workspace.claim.job._id,
path: change.path,
source,
changeType: change.changeType,
diff: truncate(diff, 50_000),
});
}
if (changes.length > 0) {
await appendEvent(
workspace.claim.job._id,
'info',
'edit',
`Workspace has ${changes.length} changed file${
changes.length === 1 ? '' : 's'
}.`,
JSON.stringify(changes),
);
}
};
const materializeEnvFile = async (workspace: ActiveWorkspace) => {
const { claim, repoDir } = workspace;
if (!claim.job.materializeEnvFile || !claim.job.envFilePath) return;
@@ -1085,6 +1180,9 @@ const runClaim = async (claim: Claim) => {
].filter(Boolean);
const redact = createRedactor(secretValues);
try {
if ((claim.job.runtime ?? 'opencode') !== 'opencode') {
throw new Error('Legacy OpenAI direct jobs are no longer supported.');
}
await updateStatus(jobId, 'preparing');
await appendEvent(jobId, 'info', 'clone', 'Creating installation token.');
if (!claim.github.installationId) {
@@ -1251,16 +1349,11 @@ export const runWorkspaceCommand = async (jobId: string, command: string) => {
redact: workspace.redact,
});
await updateStatus(workspace.claim.job._id, 'running');
await recordWorkspaceChange({
jobId: workspace.claim.job._id,
path: '.',
source: 'command',
changeType: 'modified',
diff: truncate(
(await getWorktreeDiff(workspace.repoDir, workspace.redact)).output,
50_000,
),
});
await recordChangedFiles(
workspace,
'command',
(await getWorktreeDiff(workspace.repoDir, workspace.redact)).output,
);
return { success: true };
};
@@ -1347,9 +1440,6 @@ export const sendWorkspaceMessage = async (jobId: string, prompt: string) => {
await appendEvent(claim.job._id, 'info', 'plan', 'Sending message to agent.');
try {
if ((claim.job.runtime ?? 'opencode') !== 'opencode') {
throw new Error('Legacy OpenAI direct jobs are no longer supported.');
}
workspace.agentTurnActive = true;
const assistantMessageId = await appendMessage({
jobId: claim.job._id,
@@ -1430,13 +1520,7 @@ export const sendWorkspaceMessage = async (jobId: string, prompt: string) => {
content: truncate(diff.output, 200_000),
contentType: 'text/x-diff',
});
await recordWorkspaceChange({
jobId: claim.job._id,
path: '.',
source: 'agent',
changeType: 'modified',
diff: truncate(diff.output, 50_000),
});
await recordChangedFiles(workspace, 'agent', diff.output);
} catch (error) {
workspace.agentTurnActive = false;
workspace.resolveTurn?.();