Fix agent empty-response in prod: workdir mount, image freshness, error surfacing
- Pin codex@0.142.0 + opencode-ai@1.17.9 in the job image (was @latest, causing dev/prod drift) - Worker now s the job image once per process so prod stops running a stale Codex - Surface Codex error/turn.failed events instead of swallowing them, so the real failure reason is reported rather than 'no assistant response' - Harden the Codex JSON parser to also handle the legacy msg-wrapped shape - Fix the docker-in-docker workdir: bind-mount identical host:container path and set SPOON_AGENT_HOST_WORKDIR (named volume can't be mounted by sibling job containers) - Add docs/compose.prod.yml as a documented reference deployment
This commit is contained in:
@@ -111,6 +111,10 @@ type ActiveWorkspace = {
|
||||
agentTurnActive?: boolean;
|
||||
resolveTurn?: () => void;
|
||||
lastRecordedDiffSignature?: string;
|
||||
// Captures the most recent Codex `error`/`turn.failed` event for the active
|
||||
// turn so the failure surfaces the real reason instead of a generic
|
||||
// "no assistant response" message.
|
||||
codexTurnError?: string;
|
||||
};
|
||||
|
||||
type FileTreeNode = {
|
||||
@@ -599,6 +603,11 @@ const handleAgentEvent = async (args: {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// event.kind === 'error'
|
||||
// Record the real Codex failure reason on the workspace so the turn can
|
||||
// surface it (Codex can emit `error`/`turn.failed` events and still exit 0
|
||||
// in some versions, which otherwise looks like an empty response).
|
||||
workspace.codexTurnError = event.message;
|
||||
await appendEvent(jobId, 'error', 'plan', truncate(event.message, 20_000));
|
||||
};
|
||||
|
||||
@@ -683,6 +692,12 @@ const workspaceCurrentContent = new Map<
|
||||
}
|
||||
>();
|
||||
|
||||
// Reading through a function boundary prevents TypeScript from narrowing the
|
||||
// field to `undefined` after the synchronous reset in `runCodexTurn`; it is set
|
||||
// asynchronously by the stream event handler.
|
||||
const readCodexTurnError = (workspace: ActiveWorkspace) =>
|
||||
workspace.codexTurnError;
|
||||
|
||||
const runCodexTurn = async (args: {
|
||||
workspace: ActiveWorkspace;
|
||||
prompt: string;
|
||||
@@ -691,6 +706,7 @@ const runCodexTurn = async (args: {
|
||||
}) => {
|
||||
const { workspace, prompt, assistantMessageId, assistantContent } = args;
|
||||
workspace.runtimeMode = 'codex_exec';
|
||||
workspace.codexTurnError = undefined;
|
||||
await setRuntimeSession({
|
||||
jobId: workspace.claim.job._id,
|
||||
agentRuntimeMode: 'codex_exec',
|
||||
@@ -813,6 +829,15 @@ const runCodexTurn = async (args: {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Codex can report a failure via a JSON `error`/`turn.failed` event while
|
||||
// still exiting 0. If the turn produced no assistant text but did report an
|
||||
// error, surface that real reason rather than a generic empty response.
|
||||
// Read through a helper so it is not narrowed away by the reset above (the
|
||||
// field is mutated asynchronously inside the stream handler).
|
||||
const codexTurnError = readCodexTurnError(workspace);
|
||||
if (!assistantContent.value.trim() && codexTurnError) {
|
||||
throw new Error(`codex failed:\n${codexTurnError}`);
|
||||
}
|
||||
};
|
||||
|
||||
const runOpenCodeTurn = async (args: {
|
||||
@@ -1593,7 +1618,9 @@ export const sendWorkspaceMessage = async (
|
||||
`Codex completed without producing an assistant response for job ${claim.job._id}.`,
|
||||
);
|
||||
throw new Error(
|
||||
'Codex completed without producing an assistant response.',
|
||||
workspace.codexTurnError
|
||||
? `Codex failed: ${workspace.codexTurnError}`
|
||||
: 'Codex completed without producing an assistant response.',
|
||||
);
|
||||
}
|
||||
await updateMessage({
|
||||
|
||||
Reference in New Issue
Block a user