zeroclaw-labs/zeroclaw · warning · anyhow::Error
Call not found: {call_id}
Error message
Call not found: {call_id} What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/voice_call.rs:371
}
::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");
}
}
/// Save call transcript to workspace (if logging is enabled).
pub async fn save_transcript(
&self,
call_id: &str,
workspace_dir: &std::path::Path,
) -> Result<()> {
if !self.config.transcription_logging {
return Ok(());
}
let calls = self.active_calls.lock().await;
let Some(record) = calls.get(call_id) else {
bail!("Call not found: {call_id}");
};
let logs_dir = workspace_dir.join("logs").join("calls");
std::fs::create_dir_all(&logs_dir)?;
let filename = format!("{}_{}.json", record.started_at.replace(':', "-"), call_id);
let path = logs_dir.join(filename);
let json = serde_json::to_string_pretty(record)?;
std::fs::write(&path, json)?;
::zeroclaw_log::record!(
INFO,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(
::serde_json::json!({"call_id": call_id, "path": path.display().to_string()})
),
"call transcript saved"
);
Ok(())View on GitHub (pinned to 88bb9c8533)
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.
Example fix
// before — id from place_call was never inserted into active_calls
let id = voice.place_call("+15551234567").await?; // outbound: NOT tracked
voice.save_transcript(&id, &workspace).await?; // -> "Call not found: ..."
// after — guard with get_call and only save inbound-tracked calls
if voice.get_call(&id).await.is_some() {
voice.save_transcript(&id, &workspace).await?;
} else {
::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
"skipping transcript save for untracked call");
} Defensive patterns
Strategy: validation
Validate before calling
// Only save transcripts for calls the channel actually tracks.
if voice.get_call(call_id).await.is_some() {
voice.save_transcript(call_id, &workspace_dir).await?;
} Try / catch
match voice.save_transcript(call_id, &workspace_dir).await {
Ok(()) => {}
Err(e) if e.to_string().starts_with("Call not found:") => {
// Benign for untracked/outbound calls: skip rather than crash the caller.
}
Err(e) => return Err(e), // IO / serialization errors are real failures
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- interaction reply target unknown or expired (id {interaction
- Voice Call channel requires the `channel-voice-call` feature
- Twilio call failed: {body}
- Telnyx call failed: {body}
- Plivo call failed: {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/f4989a4fa9efff9e.
Report an issue: GitHub.