Update stuff
This commit is contained in:
@@ -12,6 +12,8 @@ const requiredEnv = (name: string) => {
|
||||
};
|
||||
|
||||
export const env = {
|
||||
buildSha: process.env.SPOON_BUILD_SHA?.trim() ?? 'development',
|
||||
buildCreatedAt: process.env.SPOON_BUILD_CREATED_AT?.trim() ?? 'unknown',
|
||||
convexUrl:
|
||||
process.env.NEXT_PUBLIC_CONVEX_URL?.trim() ??
|
||||
process.env.CONVEX_SELF_HOSTED_URL?.trim() ??
|
||||
@@ -31,6 +33,7 @@ export const env = {
|
||||
jobImage:
|
||||
process.env.SPOON_AGENT_JOB_IMAGE?.trim() ?? 'spoon-agent-job:latest',
|
||||
workdir: process.env.SPOON_AGENT_WORKDIR?.trim() ?? '.local/agent-work',
|
||||
hostWorkdir: process.env.SPOON_AGENT_HOST_WORKDIR?.trim(),
|
||||
network: process.env.SPOON_AGENT_NETWORK?.trim(),
|
||||
pollMs: intEnv('SPOON_AGENT_POLL_MS', 5_000),
|
||||
httpPort: intEnv('SPOON_AGENT_WORKER_HTTP_PORT', 3921),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execa } from 'execa';
|
||||
import path from 'node:path';
|
||||
|
||||
import { env } from '../env';
|
||||
|
||||
@@ -17,13 +18,25 @@ const networkArgs = () => (env.network ? ['--network', env.network] : []);
|
||||
|
||||
const containerRuntime = () => env.containerRuntime;
|
||||
|
||||
const hostWorkspacePath = (workdir: string) => {
|
||||
if (!env.hostWorkdir) return workdir;
|
||||
const workerRoot = path.resolve(env.workdir);
|
||||
const resolvedWorkdir = path.resolve(workdir);
|
||||
const relative = path.relative(workerRoot, resolvedWorkdir);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
return workdir;
|
||||
}
|
||||
return path.join(env.hostWorkdir, relative);
|
||||
};
|
||||
|
||||
export const jobWorkspaceVolumeSpec = (workdir: string) => {
|
||||
const volumeOptions =
|
||||
env.containerVolumeOptions ??
|
||||
(containerRuntime().endsWith('podman') ? 'Z' : undefined);
|
||||
const source = hostWorkspacePath(workdir);
|
||||
return volumeOptions
|
||||
? `${workdir}:/workspace:${volumeOptions}`
|
||||
: `${workdir}:/workspace`;
|
||||
? `${source}:/workspace:${volumeOptions}`
|
||||
: `${source}:/workspace`;
|
||||
};
|
||||
|
||||
export const runInJobContainer = async (args: {
|
||||
|
||||
@@ -167,6 +167,9 @@ export const startWorkerServer = () => {
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`Worker HTTP ${request.method ?? 'UNKNOWN'} ${request.url ?? '/'} failed: ${message}`,
|
||||
);
|
||||
const status =
|
||||
message === 'Unauthorized'
|
||||
? 401
|
||||
|
||||
@@ -696,6 +696,17 @@ const runCodexTurn = async (args: {
|
||||
agentRuntimeMode: 'codex_exec',
|
||||
codexSessionId: workspace.codexSessionId,
|
||||
});
|
||||
const outputFileName = `last-message-${workspace.claim.job._id}.txt`;
|
||||
const outputFileHostPath = path.join(
|
||||
workspace.workdir,
|
||||
'.codex',
|
||||
outputFileName,
|
||||
);
|
||||
const outputFileContainerPath = path.posix.join(
|
||||
codexContainerWorkspace,
|
||||
'.codex',
|
||||
outputFileName,
|
||||
);
|
||||
const command = workspace.codexSessionId
|
||||
? [
|
||||
'codex',
|
||||
@@ -704,6 +715,8 @@ const runCodexTurn = async (args: {
|
||||
'--json',
|
||||
...codexModelArgs(workspace.claim),
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
'--output-last-message',
|
||||
outputFileContainerPath,
|
||||
workspace.codexSessionId,
|
||||
prompt,
|
||||
]
|
||||
@@ -713,6 +726,8 @@ const runCodexTurn = async (args: {
|
||||
'--json',
|
||||
...codexModelArgs(workspace.claim),
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
'--output-last-message',
|
||||
outputFileContainerPath,
|
||||
'--cd',
|
||||
codexContainerRepo,
|
||||
prompt,
|
||||
@@ -756,9 +771,48 @@ const runCodexTurn = async (args: {
|
||||
}
|
||||
},
|
||||
});
|
||||
await appendEvent(
|
||||
workspace.claim.job._id,
|
||||
'info',
|
||||
'plan',
|
||||
`Codex CLI exited with code ${result.exitCode}; captured output length ${result.output.length}; assistant length ${assistantContent.value.length}.`,
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`codex failed:\n${result.output}`);
|
||||
}
|
||||
if (!assistantContent.value.trim()) {
|
||||
try {
|
||||
const lastMessage = await readFile(outputFileHostPath, 'utf8');
|
||||
if (lastMessage.trim()) {
|
||||
assistantContent.value = truncate(
|
||||
workspace.redact(lastMessage.trim()),
|
||||
40_000,
|
||||
);
|
||||
await updateMessage({
|
||||
messageId: assistantMessageId,
|
||||
content: assistantContent.value,
|
||||
status: 'streaming',
|
||||
});
|
||||
await appendEvent(
|
||||
workspace.claim.job._id,
|
||||
'info',
|
||||
'plan',
|
||||
`Recovered assistant response from Codex output file ${outputFileName}.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const code = error && typeof error === 'object' ? 'code' in error : false;
|
||||
if (!code || (error as { code?: string }).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
await appendEvent(
|
||||
workspace.claim.job._id,
|
||||
'warn',
|
||||
'plan',
|
||||
`Codex output file ${outputFileName} was not created.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const runOpenCodeTurn = async (args: {
|
||||
@@ -1249,6 +1303,12 @@ const runClaim = async (claim: Claim) => {
|
||||
'Workspace is ready. You can browse files, edit manually, run commands, or send messages to the agent.',
|
||||
});
|
||||
await appendEvent(jobId, 'info', 'plan', 'Interactive workspace is ready.');
|
||||
await appendEvent(
|
||||
jobId,
|
||||
'info',
|
||||
'plan',
|
||||
`Worker runtime ${env.workerId} build ${env.buildSha} (${env.buildCreatedAt}).`,
|
||||
);
|
||||
|
||||
await sendWorkspaceMessage(jobId, systemPromptForJob(claim), {
|
||||
recordUserMessage: false,
|
||||
@@ -1488,6 +1548,9 @@ export const sendWorkspaceMessage = async (
|
||||
assistantMessageId,
|
||||
assistantContent,
|
||||
});
|
||||
console.log(
|
||||
`Codex turn completed for job ${claim.job._id}; response length=${assistantContent.value.length}`,
|
||||
);
|
||||
} else if (env.runtime === 'docker') {
|
||||
await appendEvent(
|
||||
claim.job._id,
|
||||
@@ -1526,6 +1589,9 @@ export const sendWorkspaceMessage = async (
|
||||
}
|
||||
if (isCodexLoginProfile(claim)) {
|
||||
if (!assistantContent.value.trim()) {
|
||||
console.error(
|
||||
`Codex completed without producing an assistant response for job ${claim.job._id}.`,
|
||||
);
|
||||
throw new Error(
|
||||
'Codex completed without producing an assistant response.',
|
||||
);
|
||||
@@ -1570,6 +1636,7 @@ export const sendWorkspaceMessage = async (
|
||||
workspace.resolveTurn?.();
|
||||
workspace.resolveTurn = undefined;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Agent turn failed for job ${claim.job._id}: ${message}`);
|
||||
await appendEvent(
|
||||
claim.job._id,
|
||||
'error',
|
||||
@@ -1692,6 +1759,8 @@ export const getWorkerHealth = async () => {
|
||||
const containerNames = await listWorkspaceContainerNames('spoon-agent-job-');
|
||||
return {
|
||||
ok: true,
|
||||
buildSha: env.buildSha,
|
||||
buildCreatedAt: env.buildCreatedAt,
|
||||
workerId: env.workerId,
|
||||
convexUrl: env.convexUrl,
|
||||
runtime: env.runtime,
|
||||
@@ -1699,6 +1768,7 @@ export const getWorkerHealth = async () => {
|
||||
containerAccess: env.containerAccess,
|
||||
jobImage: env.jobImage,
|
||||
workdir: env.workdir,
|
||||
hostWorkdir: env.hostWorkdir,
|
||||
network: env.network,
|
||||
httpPort: env.httpPort,
|
||||
maxConcurrentJobs: env.maxConcurrentJobs,
|
||||
|
||||
Reference in New Issue
Block a user