{"record":{"id":"c4e765a63c54e6b1","repo":"aaif-goose/goose","slug":"request-already-completed","errorCode":null,"errorMessage":"Request already completed: {}","messagePattern":"Request already completed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/goose/src/action_required_manager.rs","lineNumber":38,"sourceCode":"}\n\nstruct PendingRequest {\n    session_id: String,\n    response_tx: Option<tokio::sync::oneshot::Sender<ElicitationOutcome>>,\n}\n\npub(crate) struct PendingResponseClaim {\n    request_id: String,\n    pending: OwnedMutexGuard<PendingRequest>,\n}\n\nimpl PendingResponseClaim {\n    pub(crate) fn submit(mut self, response: ElicitationOutcome) -> Result<()> {\n        let tx = self\n            .pending\n            .response_tx\n            .take()\n            .ok_or_else(|| anyhow::anyhow!(\"Request already completed: {}\", self.request_id))?;\n        drop(self.pending);\n\n        if tx.send(response).is_err() {\n            return Err(anyhow::anyhow!(\"Response channel closed\"));\n        }\n\n        Ok(())\n    }\n}\n\npub(crate) struct ActionRequiredManager {\n    pending: Arc<RwLock<HashMap<String, Arc<Mutex<PendingRequest>>>>>,\n    action_required_senders: Mutex<HashMap<(String, String), mpsc::Sender<Message>>>,\n}\n\nimpl ActionRequiredManager {\n    pub(crate) fn new() -> Self {\n        Self {","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/crates/goose/src/action_required_manager.rs#L20-L56","documentation":"PendingResponseClaim::submit moves the response_tx out of the pending request via take(); a second submit on the same claim (or on a request whose tx was already consumed) finds None and fails with this message. It enforces exactly-one-response semantics for action-required elicitations — the guard against double answering.","triggerScenarios":"Two code paths respond to the same elicitation request id: a UI button handler and a programmatic responder both calling submit; or a retry of a submit that already succeeded.","commonSituations":"Client code that answers an elicitation and then answers again on timeout/cancel; duplicated event handlers firing for one action; race between an explicit user answer and an automatic timeout responder.","solutions":["Ensure only one responder owns the claim — consume PendingResponseClaim exactly once per request id","Deduplicate at the call site: guard with an atomic 'answered' flag or drop the claim after first use","Treat a second submit as a no-op (catch and ignore 'Request already completed') rather than an error path"],"exampleFix":"// before\nclaim.submit(outcome).await?;\nclaim.submit(outcome).await?; // duplicate\n\n// after\nif let Err(e) = claim.submit(outcome).await {\n    tracing::debug!(%e, \"elicitation already answered\");\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(e) = claim.submit(outcome).await {\n    if e.to_string().contains(\"already completed\") {\n        tracing::debug!(\"duplicate elicitation response ignored\");\n        return Ok(()); // first response stands\n    }\n    return Err(e);\n}","preventionTips":["Consume PendingResponseClaim exactly once; move it into the single responder","Disable UI submit controls after the first response"],"tags":["elicitation","idempotency","channel","double-submit"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}