diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 7cbd5c74945..9b5f0390842 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -276,6 +276,32 @@ describe("createAcpxRuntimeService", () => { await service.stop?.(ctx); }); + it("formats non-string doctor details without losing object payloads", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = createMockRuntime({ + doctor: async () => ({ + ok: false, + message: "probe failed", + details: [{ code: "ACP_CLOSED", agent: "codex" }, new Error("stdin closed")], + }), + isHealthy: () => false, + }); + const service = createAcpxRuntimeService({ + runtimeFactory: () => runtime as never, + }); + + await service.start(ctx); + + await vi.waitFor(() => { + expect(ctx.logger.warn).toHaveBeenCalledWith( + 'embedded acpx runtime backend probe failed: probe failed ({"code":"ACP_CLOSED","agent":"codex"}; stdin closed)', + ); + }); + + await service.stop?.(ctx); + }); + it("can skip the embedded runtime backend via env", async () => { process.env.OPENCLAW_SKIP_ACPX_RUNTIME = "1"; const workspaceDir = await makeTempDir(); diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index c7065a174fe..df0537497fa 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -1,4 +1,5 @@ import fs from "node:fs/promises"; +import { inspect } from "node:util"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { AcpRuntime, @@ -79,8 +80,36 @@ function warnOnIgnoredLegacyCompatibilityConfig(params: { ); } -function formatDoctorFailureMessage(report: { message: string; details?: string[] }): string { - const detailText = report.details?.filter(Boolean).join("; ").trim(); +function formatDoctorDetail(detail: unknown): string | null { + if (!detail) { + return null; + } + if (typeof detail === "string") { + return detail.trim() || null; + } + if (detail instanceof Error) { + return formatErrorMessage(detail); + } + if (typeof detail === "object") { + try { + return JSON.stringify(detail) ?? inspect(detail, { breakLength: Infinity, depth: 3 }); + } catch { + return inspect(detail, { breakLength: Infinity, depth: 3 }); + } + } + if ( + typeof detail === "number" || + typeof detail === "boolean" || + typeof detail === "bigint" || + typeof detail === "symbol" + ) { + return detail.toString(); + } + return inspect(detail, { breakLength: Infinity, depth: 3 }); +} + +function formatDoctorFailureMessage(report: { message: string; details?: unknown[] }): string { + const detailText = report.details?.map(formatDoctorDetail).filter(Boolean).join("; ").trim(); return detailText ? `${report.message} (${detailText})` : report.message; }