Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Repo: https://github.com/openclaw/acpx
- Flows: coalesce heartbeat writes while storage is busy so slow filesystems do not accumulate overlapping writes and stall running steps.
- Flows: keep the host alive when a shell action closes stdin before consuming its input. Thanks @SebTardif.
- ACP/terminal: handle child stdout and stderr errors without terminating the host, so wait and release can finish. Thanks @SebTardif.
- ACP/launch: preserve process-spawn `ENOENT` as additive `AGENT_SPAWN_ENOENT` detail and include qualified remediation while keeping the broad runtime code and other spawn failures unchanged. Fixes #510. Thanks @anyech.

## 2026.8.28 (v0.13.2)

Expand Down
7 changes: 7 additions & 0 deletions docs/ACPX_ERROR_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ Auth-required policy:
- use `detailCode=AUTH_REQUIRED` for deterministic machine handling
- include raw ACP payload in `acp` when available (for example `acp.code=-32000`)

Agent-spawn policy:

- keep top-level `code` as `RUNTIME` for compatibility
- use `detailCode=AGENT_SPAWN_ENOENT` when process creation fails with `ENOENT`
- describe the missing launch path without assuming it is always the command binary; the executable, interpreter, or working directory may be absent
- leave non-`ENOENT` spawn failures on the generic runtime path

## Queue detail codes (initial set)

- `QUEUE_OWNER_CLOSED`
Expand Down
12 changes: 11 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,18 @@ export class AgentSpawnError extends AcpxOperationalError {
readonly agentCommand: string;

constructor(agentCommand: string, cause?: unknown) {
super(`Failed to spawn agent command: ${agentCommand}`, {
const spawnCode = cause instanceof Error ? (cause as NodeJS.ErrnoException).code : undefined;
const spawnEnoent = spawnCode === "ENOENT";
const message = spawnEnoent
? `Failed to spawn agent command: ${agentCommand}. The agent process could not start because a required executable, interpreter, working directory, or other launch path was not found. Check the command, effective PATH, and working directory, or verify the custom agent's configured argv.`
: `Failed to spawn agent command: ${agentCommand}`;
super(message, {
cause: cause instanceof Error ? cause : undefined,
...(spawnEnoent
? {
detailCode: "AGENT_SPAWN_ENOENT",
}
: {}),
});
this.agentCommand = agentCommand;
}
Expand Down
47 changes: 47 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,53 @@ test("CLI resolves unknown raw agent commands after newer global flags", async (
});
});

test(
"CLI reports actionable text and JSON detail for a missing raw agent command",
{ skip: process.platform === "win32" },
async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const missingAgent = path.join(homeDir, "missing-raw-agent");

const text = await runCli(["--cwd", cwd, missingAgent, "exec", "ping"], homeDir);
assert.equal(text.code, 1);
assert.match(text.stderr, /required executable, interpreter, working directory/i);
assert.match(text.stderr, /effective PATH/i);
assert.match(text.stderr, /configured argv/i);

const json = await runCli(
["--cwd", cwd, "--format", "json", missingAgent, "exec", "ping"],
homeDir,
);
assert.equal(json.code, 1);
const error = parseSingleAcpErrorLine(json.stdout);
assert.equal(error.data?.acpxCode, "RUNTIME");
assert.equal(error.data?.detailCode, "AGENT_SPAWN_ENOENT");
assert.equal(error.data?.origin, "cli");

const missingCwd = await runCli(
[
"--cwd",
path.join(homeDir, "missing-workspace"),
"--format",
"json",
"--agent",
process.execPath,
"exec",
"ping",
],
homeDir,
);
assert.equal(missingCwd.code, 1);
const missingCwdError = parseSingleAcpErrorLine(missingCwd.stdout);
assert.equal(missingCwdError.data?.acpxCode, "RUNTIME");
assert.equal(missingCwdError.data?.detailCode, "AGENT_SPAWN_ENOENT");
assert.match(missingCwdError.message ?? "", /working directory/i);
});
},
);

test("global passthrough flags are present in help output", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(["--help"], homeDir);
Expand Down
31 changes: 31 additions & 0 deletions test/error-normalization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,42 @@ import {
isAcpResourceNotFoundError,
} from "../src/acp/error-normalization.js";
import {
AgentSpawnError,
PermissionPromptUnavailableError,
QueueConnectionError,
AuthPolicyError,
} from "../src/errors.js";

test("normalizeOutputError preserves spawn ENOENT as an additive detail", () => {
const cause = Object.assign(new Error("spawn custom-agent ENOENT"), {
code: "ENOENT",
});
const normalized = normalizeOutputError(new AgentSpawnError("custom-agent", cause), {
origin: "cli",
});

assert.equal(normalized.code, "RUNTIME");
assert.equal(normalized.detailCode, "AGENT_SPAWN_ENOENT");
assert.equal(normalized.origin, "cli");
assert.match(normalized.message, /^Failed to spawn agent command: custom-agent\./);
assert.match(normalized.message, /required executable, interpreter, working directory/i);
assert.match(normalized.message, /effective PATH/i);
assert.match(normalized.message, /configured argv/i);
});

test("normalizeOutputError keeps non-ENOENT spawn failures generic", () => {
const cause = Object.assign(new Error("spawn custom-agent EACCES"), {
code: "EACCES",
});
const normalized = normalizeOutputError(new AgentSpawnError("custom-agent", cause), {
origin: "cli",
});

assert.equal(normalized.code, "RUNTIME");
assert.equal(normalized.detailCode, undefined);
assert.equal(normalized.message, "Failed to spawn agent command: custom-agent");
});

test("normalizeOutputError maps permission prompt unavailable errors", () => {
const normalized = normalizeOutputError(new PermissionPromptUnavailableError(), {
origin: "runtime",
Expand Down