Archived
fix: improve executor failure handling
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
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";
|
import { log } from "../../core/contracts.js";
|
||||||
|
|
||||||
type OpenCodeClient = ReturnType<typeof createOpencodeClient>;
|
type OpenCodeClient = ReturnType<typeof createOpencodeClient>;
|
||||||
@@ -27,12 +27,23 @@ export async function withPromptTransport<T>(
|
|||||||
bodyTimeout: 0,
|
bodyTimeout: 0,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
return await operation((input, init) =>
|
const customFetch: typeof fetch = async (input, init) => {
|
||||||
fetch(input, {
|
const request = new Request(input, init);
|
||||||
...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,
|
dispatcher,
|
||||||
} as RequestInit & { dispatcher: Dispatcher }),
|
});
|
||||||
);
|
return response as unknown as Response;
|
||||||
|
};
|
||||||
|
return await operation(customFetch);
|
||||||
} finally {
|
} finally {
|
||||||
await dispatcher.close();
|
await dispatcher.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function publishJob(
|
|||||||
context.botLogin,
|
context.botLogin,
|
||||||
job,
|
job,
|
||||||
`Agent ${job.mode} failed`,
|
`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" };
|
return { terminal: "failed" };
|
||||||
}
|
}
|
||||||
@@ -39,3 +39,12 @@ export async function publishJob(
|
|||||||
? publishPlan(context, job)
|
? publishPlan(context, job)
|
||||||
: publishImplementation(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}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { createServer } from "node:http";
|
||||||
import test from "node:test";
|
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 () => {
|
test("returns completed OpenCode work without finalization", async () => {
|
||||||
let stopped = false;
|
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
@@ -42,7 +42,7 @@ CMD ["node", "/opt/ci-agents/dist/application/controller/main.js"]
|
|||||||
|
|
||||||
FROM runtime AS executor
|
FROM runtime AS executor
|
||||||
ARG TARGETARCH=amd64
|
ARG TARGETARCH=amd64
|
||||||
ARG OPENCODE_VERSION=1.18.2
|
ARG OPENCODE_VERSION=1
|
||||||
ARG GITEA_MCP_VERSION=1.3.0
|
ARG GITEA_MCP_VERSION=1.3.0
|
||||||
USER root
|
USER root
|
||||||
RUN npm install --global --omit=dev "opencode-ai@${OPENCODE_VERSION}" \
|
RUN npm install --global --omit=dev "opencode-ai@${OPENCODE_VERSION}" \
|
||||||
|
|||||||
Reference in New Issue
Block a user