{"record":{"id":"2a598fd39fd7ab5d","repo":"zeroclaw-labs/zeroclaw","slug":"acp-request-permission-timed-out-after-timeout","errorCode":null,"errorMessage":"ACP request_permission timed out after {timeout:?}","messagePattern":"ACP request_permission timed out after (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/acp_channel.rs","lineNumber":97,"sourceCode":"            \"options\": options,\n            // `toolCall` is required by the ACP schema. We use a synthetic\n            // ask_user tool call so the client surfaces the prompt with a\n            // sensible title.\n            \"toolCall\": {\n                \"toolCallId\": format!(\"ask-user-{}\", uuid::Uuid::new_v4()),\n                \"title\": question,\n                \"kind\": \"other\",\n                \"status\": \"pending\",\n            }\n        });\n\n        let call = self.rpc.request(\"session/request_permission\", params);\n        let response = match tokio::time::timeout(timeout, call).await {\n            Ok(Ok(value)) => value,\n            Ok(Err(e)) => {\n                anyhow::bail!(\"ACP request_permission failed: {} ({})\", e.message, e.code)\n            }\n            Err(_) => anyhow::bail!(\"ACP request_permission timed out after {timeout:?}\"),\n        };\n\n        // Response shape: { outcome: { outcome: \"selected\", optionId: \"...\" } | { outcome: \"cancelled\" } }\n        let outcome = response.get(\"outcome\");\n        let kind = outcome\n            .and_then(|o| o.get(\"outcome\"))\n            .and_then(|s| s.as_str())\n            .unwrap_or(\"\");\n        match kind {\n            \"selected\" => {\n                let option_id = outcome\n                    .and_then(|o| o.get(\"optionId\"))\n                    .and_then(|s| s.as_str())\n                    .unwrap_or(\"\");\n                let idx = option_id\n                    .strip_prefix(\"choice-\")\n                    .and_then(|s| s.parse::<usize>().ok());\n                match idx.and_then(|i| choices.get(i)) {","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/acp_channel.rs#L79-L115","documentation":"The companion check in AzureOpenAiBuilder::build(): after resource_name, it reads deployment_name with this expect. The deployment name is the second half of the required URL pair — without it the https://{resource}.openai.azure.com/openai/deployments/{deployment} endpoint cannot be formed, which is why the builder documents the panic. It fires when deployment_name() was never called on the builder.","triggerScenarios":"build() invoked after setting resource_name but not deployment_name — e.g. config supplied the Azure resource yet omitted the deployment/model deployment id.","commonSituations":"Azure provider config that names the resource but not the deployment; deployments renamed in Azure portal so old config keys no longer map; test or tooling code constructing the builder with only one of the two required setters.","solutions":["Set deployment_name() to the Azure deployment id (not the model name) before build().","Validate both fields together when parsing provider config, failing with one actionable message listing both keys.","Prefer a constructor taking resource and deployment as parameters so the type system enforces presence.","Keep the listed builder tests green as a regression net."],"exampleFix":"// before\nlet provider = AzureOpenAiModelProvider::builder()\n    .resource_name(\"my-resource\")\n    .build(); // panics: deployment_name() is required\n\n// after\nlet provider = AzureOpenAiModelProvider::builder()\n    .resource_name(\"my-resource\")\n    .deployment_name(\"gpt-4o-deploy\")\n    .build();","handlingStrategy":"validation","validationCode":"// Check both halves of the URL pair before build():\nfn azure_url_inputs_present(cfg: &AzureProviderConfig) -> Result<(), String> {\n    match (&cfg.resource_name, &cfg.deployment_name) {\n        (Some(r), Some(d)) if !r.is_empty() && !d.is_empty() => Ok(()),\n        _ => Err(\"azure provider needs both resource_name and deployment_name\".into()),\n    }\n}","typeGuard":"fn azure_deployment_set(cfg: &serde_json::Value) -> bool {\n    cfg.get(\"deployment_name\")\n        .and_then(|v| v.as_str())\n        .map_or(false, |s| !s.trim().is_empty())\n}","tryCatchPattern":"let p = std::panic::catch_unwind(|| {\n    AzureOpenAiModelProvider::builder()\n        .resource_name(r.clone())\n        .deployment_name(d.clone())\n        .build()\n});\nif p.is_err() { /* surface a config error naming resource_name and deployment_name */ }","preventionTips":["Use the Azure deployment id, not the model name, for deployment_name","Validate resource+deployment together so one message covers both panics","Alert on portal-side deployment renames that invalidate stored config","Prefer required-parameter constructors in your own builders"],"tags":["rust","builder-pattern","azure","openai","config-validation","panic"],"backgroundTag":"builder-missing-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}