{"record":{"id":"f4989a4fa9efff9e","repo":"zeroclaw-labs/zeroclaw","slug":"call-not-found-call-id","errorCode":null,"errorMessage":"Call not found: {call_id}","messagePattern":"Call not found: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/voice_call.rs","lineNumber":371,"sourceCode":"            }\n\n            ::zeroclaw_log::record!(DEBUG, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({\"call_id\": call_id, \"old_state\": old_state, \"new_state\": new_state})), \"call state transition\");\n        }\n    }\n\n    /// Save call transcript to workspace (if logging is enabled).\n    pub async fn save_transcript(\n        &self,\n        call_id: &str,\n        workspace_dir: &std::path::Path,\n    ) -> Result<()> {\n        if !self.config.transcription_logging {\n            return Ok(());\n        }\n\n        let calls = self.active_calls.lock().await;\n        let Some(record) = calls.get(call_id) else {\n            bail!(\"Call not found: {call_id}\");\n        };\n\n        let logs_dir = workspace_dir.join(\"logs\").join(\"calls\");\n        std::fs::create_dir_all(&logs_dir)?;\n\n        let filename = format!(\"{}_{}.json\", record.started_at.replace(':', \"-\"), call_id);\n        let path = logs_dir.join(filename);\n        let json = serde_json::to_string_pretty(record)?;\n        std::fs::write(&path, json)?;\n\n        ::zeroclaw_log::record!(\n            INFO,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(\n                ::serde_json::json!({\"call_id\": call_id, \"path\": path.display().to_string()})\n            ),\n            \"call transcript saved\"\n        );\n        Ok(())","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/voice_call.rs#L353-L389","documentation":"Raised by VoiceCallChannel::save_transcript when transcription_logging is enabled but call_id is not present in the in-memory active_calls map. Records are only inserted by handle_inbound_call (inbound webhooks); notably place_call/execute_outbound_call never inserts the returned call id into active_calls. The lookup holds the mutex, so the failure is purely a map-miss, not a lock or IO problem — the transcript file is never written when this fires.","triggerScenarios":"save_transcript(\"CA123...\", ws) where (1) the id came from place_call for an outbound Telnyx/Plivo/Twilio call — outbound ids are not tracked in active_calls; (2) handle_inbound_call was never invoked for that id (status webhook misrouted); (3) the record was already removed by call cleanup; (4) typo or wrong-channel id (e.g. a Telnyx call_control_id passed to a channel configured for Twilio).","commonSituations":"Writing a cron/job that archives transcripts for all calls by iterating ids from a provider dashboard; mixing outbound ids with the inbound-only record map; testing save_transcript against a fresh VoiceCallChannel instance that never saw handle_inbound_call.","solutions":["Only call save_transcript with ids that were registered via handle_inbound_call — check with voice.get_call(call_id).await first.","For outbound calls (place_call), track the returned id yourself or extend the channel to insert a CallRecord at dial time.","Verify the status-callback webhook route actually reaches handle_inbound_call/handle_status_update for your provider.","Confirm you are not passing a different provider's id format after switching model_provider."],"exampleFix":"// before — id from place_call was never inserted into active_calls\nlet id = voice.place_call(\"+15551234567\").await?;   // outbound: NOT tracked\nvoice.save_transcript(&id, &workspace).await?;      // -> \"Call not found: ...\"\n\n// after — guard with get_call and only save inbound-tracked calls\nif voice.get_call(&id).await.is_some() {\n    voice.save_transcript(&id, &workspace).await?;\n} else {\n    ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),\n        \"skipping transcript save for untracked call\");\n}","handlingStrategy":"validation","validationCode":"// Only save transcripts for calls the channel actually tracks.\nif voice.get_call(call_id).await.is_some() {\n    voice.save_transcript(call_id, &workspace_dir).await?;\n}","typeGuard":null,"tryCatchPattern":"match voice.save_transcript(call_id, &workspace_dir).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().starts_with(\"Call not found:\") => {\n        // Benign for untracked/outbound calls: skip rather than crash the caller.\n    }\n    Err(e) => return Err(e), // IO / serialization errors are real failures\n}","preventionTips":["Remember active_calls only contains inbound ids registered by handle_inbound_call; place_call ids are not tracked.","Always precede save_transcript with get_call(call_id).await to guard the lookup.","Archive transcripts from the status-completion path (where the record is known-live) instead of external cron guessing ids.","In tests, seed the channel via handle_inbound_call before asserting on save_transcript."],"tags":["voice-call","transcript","not-found","state"],"backgroundTag":"entity-not-found","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}