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

  1. Only call save_transcript with ids that were registered via handle_inbound_call — check with voice.get_call(call_id).await first.
  2. For outbound calls (place_call), track the returned id yourself or extend the channel to insert a CallRecord at dial time.
  3. Verify the status-callback webhook route actually reaches handle_inbound_call/handle_status_update for your provider.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/f4989a4fa9efff9e. Report an issue: GitHub.