fix: improve executor failure handling

This commit is contained in:
2026-07-18 17:35:05 +02:00
parent 57fe9eafa1
commit 595005b1cf
5 changed files with 64 additions and 9 deletions
+17 -6
View File
@@ -1,5 +1,5 @@
import type { createOpencodeClient } from "@opencode-ai/sdk/v2";
import { Agent, type Dispatcher } from "undici";
import { Agent, fetch as undiciFetch } from "undici";
import { log } from "../../core/contracts.js";
type OpenCodeClient = ReturnType<typeof createOpencodeClient>;
@@ -27,12 +27,23 @@ export async function withPromptTransport<T>(
bodyTimeout: 0,
});
try {
return await operation((input, init) =>
fetch(input, {
...init,
const customFetch: typeof fetch = async (input, init) => {
const request = new Request(input, init);
const body =
request.method === "GET" || request.method === "HEAD"
? undefined
: new Uint8Array(await request.arrayBuffer());
const response = await undiciFetch(request.url, {
method: request.method,
headers: Object.fromEntries(request.headers),
...(body ? { body } : {}),
signal: request.signal,
redirect: request.redirect,
dispatcher,
} as RequestInit & { dispatcher: Dispatcher }),
);
});
return response as unknown as Response;
};
return await operation(customFetch);
} finally {
await dispatcher.close();
}
+10 -1
View File
@@ -31,7 +31,7 @@ export async function publishJob(
context.botLogin,
job,
`Agent ${job.mode} failed`,
`Request \`${job.id.slice(0, 12)}\` failed. Inspect the redacted executor and controller logs.`,
failureDetail(job.id, result.message),
);
return { terminal: "failed" };
}
@@ -39,3 +39,12 @@ export async function publishJob(
? publishPlan(context, job)
: publishImplementation(context, job);
}
export function failureDetail(jobId: string, message: string): string {
const failure = message
.replaceAll("\r\n", "\n")
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
return `Request \`${jobId.slice(0, 12)}\` failed.\n\nFailure:\n\n${failure}`;
}
+23 -1
View File
@@ -1,6 +1,28 @@
import assert from "node:assert/strict";
import { createServer } from "node:http";
import test from "node:test";
import { withWorkBudget } from "../../adapters/opencode/transport.js";
import {
withPromptTransport,
withWorkBudget,
} from "../../adapters/opencode/transport.js";
test("uses a fetch implementation compatible with the prompt dispatcher", async () => {
const server = createServer((_request, response) => response.end("ok"));
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", resolve),
);
try {
const address = server.address();
assert.ok(address && typeof address === "object");
const response = await withPromptTransport((customFetch) =>
customFetch(new Request(`http://127.0.0.1:${address.port}/prompt`)),
);
assert.equal(response.status, 200);
assert.equal(await response.text(), "ok");
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
test("returns completed OpenCode work without finalization", async () => {
let stopped = false;
@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import test from "node:test";
import { failureDetail } from "../../application/publication/service.js";
test("renders the full executor failure as literal issue comment text", () => {
assert.equal(
failureDetail(
"15f9edbf-ff6f-4b29-8231-5fc394071976",
"OpenCode prompt failed\n```embedded markdown```\nfinal detail",
),
"Request `15f9edbf-ff6` failed.\n\nFailure:\n\n OpenCode prompt failed\n ```embedded markdown```\n final detail",
);
});
+1 -1
View File
@@ -42,7 +42,7 @@ CMD ["node", "/opt/ci-agents/dist/application/controller/main.js"]
FROM runtime AS executor
ARG TARGETARCH=amd64
ARG OPENCODE_VERSION=1.18.2
ARG OPENCODE_VERSION=1
ARG GITEA_MCP_VERSION=1.3.0
USER root
RUN npm install --global --omit=dev "opencode-ai@${OPENCODE_VERSION}" \