zeroclaw-labs/zeroclaw · error · anyhow::Error
ACP request_permission timed out after {:?}
Error message
ACP request_permission timed out after {:?} What it means
The operator never answered the approval prompt within AcpChannel's approval_timeout (fixed at construction). The timeout exists so a vanished client (crash, closed IDE, dropped connection) cannot park execute_tool_call forever and hold the session slot against max_sessions — the error frees the slot and surfaces the failure. The elapsed duration is included in the message.
Source
Thrown at crates/zeroclaw-channels/src/acp_channel.rs:497
&& let Some(args) = &request.raw_arguments
&& let Some(new_text) = args.get("new_string").or_else(|| args.get("content"))
&& let Some(s) = new_text.as_str()
{
tool_call["proposedEdit"] = json!(s);
}
let params = json!({
"sessionId": self.session_id,
"options": options,
"toolCall": tool_call,
});
let call = self.rpc.request("session/request_permission", params);
let response = match tokio::time::timeout(self.approval_timeout, call).await {
Ok(Ok(value)) => value,
Ok(Err(e)) => {
anyhow::bail!("ACP request_permission failed: {} ({})", e.message, e.code)
}
Err(_) => anyhow::bail!(
"ACP request_permission timed out after {:?}",
self.approval_timeout
),
};
let outcome = response.get("outcome");
let kind = outcome
.and_then(|o| o.get("outcome"))
.and_then(|s| s.as_str())
.unwrap_or("");
match kind {
"selected" => {
let option_id = outcome
.and_then(|o| o.get("optionId"))
.and_then(|s| s.as_str())
.unwrap_or("");
// "selected" means the operator picked one of the options we
// offered, so every arm here is a genuine operator decision.View on GitHub (pinned to 88bb9c8533)
Solutions
- Treat this error as a denial (fail closed) — never as an implicit approve.
- Raise the approval_timeout passed to AcpChannel::new to fit realistic operator response times.
- Check client liveness before running approval-gated tools and skip the tool when the back channel is dead.
- Cancel in-flight approval requests on session/stop so teardown does not race the timeout.
Example fix
// construction: give operators realistic time
let ch = AcpChannel::new("acp", session_id, rpc, Duration::from_secs(600), caps);
// call site: timeout must deny, not approve
let decision = ch.request_approval(recipient, &req).await
.unwrap_or(Some(ChannelApprovalResponse::Deny))
.unwrap_or(ChannelApprovalResponse::Deny); Defensive patterns
Strategy: fallback
Try / catch
let decision = match ch.request_approval(recipient, &req).await {
Ok(d) => d.unwrap_or(ChannelApprovalResponse::Deny),
Err(e) if e.to_string().contains("timed out") => {
tracing::warn!(error = %e, "approval timed out; denying");
ChannelApprovalResponse::Deny
}
Err(e) => return Err(e),
}; Prevention
- Size approval_timeout for humans (minutes), not request round-trips
- Free the session slot on timeout — never retry approvals indefinitely
- Cancel outstanding permission requests when a session stops
When it happens
Trigger: A gated tool runs, session/request_permission is sent, and no response arrives before approval_timeout: the user ignored the dialog, the client exited without cancelling, or the transport died silently.
Common situations: approval_timeout tuned too low for real approval workflows; unattended or abandoned sessions; laptop sleep or network switches mid-approval.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- ACP elicitation/create timed out after {timeout:?}
- ACP elicitation/create (multi) timed out after {timeout:?}
- ACP returned unknown permission optionId: {other}
- ACP returned unexpected permission outcome: {other}
- ACP returned unexpected outcome: {other}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6c3a1f2c8f8f8f41.
Report an issue: GitHub.