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 @@ -7,6 +7,7 @@ Repo: https://github.com/openclaw/acpx
### Changes

- Conformance/ACP: add a data-driven ACP core v1 conformance suite with CI smoke coverage, nightly coverage, and a hardened runner that reports startup failures cleanly and scopes filesystem checks to the session cwd. (#130) Thanks @lynnzc.
- CLI/prompts: add `--prompt-retries` to retry transient prompt failures with exponential backoff while preserving strict JSON behavior and avoiding replay after prompt side effects. (#142) Thanks @lupuletic and @dutifulbob.
- Output: add `--suppress-reads` to mask raw file-read bodies in text and JSON output while keeping normal tool activity visible. (#136) Thanks @hayatosc.
- Agents/droid: add `factory-droid` and `factorydroid` aliases for the built-in Factory Droid adapter and sync the built-in docs. Thanks @vincentkoc.
- Flows/workflows: add an initial `flow run` command, an `acpx/flows` runtime surface, and file-backed flow run state under `~/.acpx/flows/runs` for user-authored workflow modules. Thanks @osolmaz.
Expand Down
2 changes: 2 additions & 0 deletions src/cli-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ async function handlePrompt(
timeoutMs: globalFlags.timeout,
ttlMs: globalFlags.ttl,
maxQueueDepth: config.queueMaxDepth,
promptRetries: globalFlags.promptRetries,
verbose: globalFlags.verbose,
waitForCompletion: flags.wait !== false,
});
Expand Down Expand Up @@ -361,6 +362,7 @@ async function handleExec(
suppressSdkConsoleErrors: outputPolicy.suppressSdkConsoleErrors,
timeoutMs: globalFlags.timeout,
verbose: globalFlags.verbose,
promptRetries: globalFlags.promptRetries,
sessionOptions: {
model: globalFlags.model,
allowedTools: globalFlags.allowedTools,
Expand Down
15 changes: 15 additions & 0 deletions src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type GlobalFlags = PermissionFlags & {
model?: string;
allowedTools?: string[];
maxTurns?: number;
promptRetries?: number;
};

export type PromptFlags = {
Expand Down Expand Up @@ -158,6 +159,14 @@ export function parseMaxTurns(value: string): number {
return parsed;
}

export function parsePromptRetries(value: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new InvalidArgumentError("Prompt retries must be a non-negative integer");
}
return parsed;
}

export function resolvePermissionMode(
flags: PermissionFlags,
defaultMode: PermissionMode,
Expand Down Expand Up @@ -209,6 +218,11 @@ export function addGlobalFlags(command: Command): Command {
parseAllowedTools,
)
.option("--max-turns <count>", "Maximum turns for the session", parseMaxTurns)
.option(
"--prompt-retries <count>",
"Retry failed prompt turns on transient errors (default: 0)",
parsePromptRetries,
)
.option(
"--json-strict",
"Strict JSON mode: requires --format json and suppresses non-JSON stderr output",
Expand Down Expand Up @@ -295,6 +309,7 @@ export function resolveGlobalFlags(command: Command, config: ResolvedAcpxConfig)
model: typeof opts.model === "string" ? parseNonEmptyValue("Model", opts.model) : undefined,
allowedTools: Array.isArray(opts.allowedTools) ? opts.allowedTools : undefined,
maxTurns: typeof opts.maxTurns === "number" ? opts.maxTurns : undefined,
promptRetries: typeof opts.promptRetries === "number" ? opts.promptRetries : undefined,
approveAll: opts.approveAll ? true : undefined,
approveReads: opts.approveReads ? true : undefined,
denyAll: opts.denyAll ? true : undefined,
Expand Down
43 changes: 43 additions & 0 deletions src/error-normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,49 @@ export function normalizeOutputError(
};
}

/**
* Returns true when an error from `client.prompt()` looks transient and
* can reasonably be retried (e.g. model-API 400/500, network hiccups that
* surface as ACP internal errors).
*
* Errors that are definitively non-recoverable (auth, missing session,
* invalid params, timeout, permission) return false.
*/
export function isRetryablePromptError(error: unknown): boolean {
if (error instanceof PermissionDeniedError || error instanceof PermissionPromptUnavailableError) {
return false;
}
if (isTimeoutLike(error) || isNoSessionLike(error) || isUsageLike(error)) {
return false;
}

// Extract ACP payload once and reuse for all subsequent checks.
const acp = extractAcpError(error);
if (!acp) {
// Non-ACP errors (e.g. process crash) are not retried at the prompt level.
return false;
}

// Resource-not-found (session gone) — check using the already-extracted payload.
if (acp.code === -32001 || acp.code === -32002) {
return false;
}

// Auth-required errors are never retryable. Use the same thorough check as normalizeOutputError.
if (isAcpAuthRequiredPayload(acp)) {
return false;
}

// Method-not-found or invalid-params are permanent protocol errors.
if (acp.code === -32601 || acp.code === -32602) {
return false;
}

// ACP internal errors (-32603) typically wrap model-API failures → retryable.
// Parse errors (-32700) can also be transient.
return acp.code === -32603 || acp.code === -32700;
}

export function exitCodeForOutputErrorCode(code: OutputErrorCode): ExitCode {
switch (code) {
case "USAGE":
Expand Down
4 changes: 4 additions & 0 deletions src/queue-owner-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export function parseQueueOwnerPayload(raw: string): QueueOwnerRuntimeOptions {
options.maxQueueDepth = Math.max(1, Math.round(record.maxQueueDepth));
}

if (typeof record.promptRetries === "number" && Number.isFinite(record.promptRetries)) {
options.promptRetries = Math.max(0, Math.round(record.promptRetries));
}

return options;
}

Expand Down
Loading