# Recovering lost calls

When a call happens but never reaches the call log: how to spot it, why nothing raises an alarm, and how the record is rebuilt from what the carrier still holds.

Source: https://docs.omazy.ai/how-to/voice/recovering-lost-calls/

import Figure from '../../../../components/Figure.astro'
import { Steps, Aside } from '@astrojs/starlight/components'

A call can happen, be answered, be recorded, and still not appear in the call
log. It is rare, it has happened, and it is worth knowing the shape of because
the failure is quiet at every step.

## Why nothing raises an alarm

A call reaches the log through one narrow path. While the call runs, the media
bridge and the carrier post lifecycle events (ringing, answered, ended,
recording ready) to the platform, and one component writes them into a single
call record. The log reads that record. Nothing else creates it.

That path is deliberately forgiving. It answers "received" the moment it takes
a valid event, before it knows whether the write worked, so a slow database
cannot hang up a live call. The cost of that choice is the failure mode here:
if the write is rejected, the caller hears a completely normal conversation,
the carrier bills a completely normal call, and the log stays empty.

<Figure
  label="Where a call can be lost between happening and appearing in the log"
  caption="Every stage before the write is a real system with its own record. Only the last one is ours, and only the last one is where the call disappears."
>
<svg viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg">
  <text x="0" y="14" class="d-eyebrow">THE ONLY PATH INTO THE CALL LOG</text>

  <rect x="0" y="34" width="150" height="50" rx="8" class="d-box" />
  <text x="14" y="56" class="d-label">Call happens</text>
  <text x="14" y="72" class="d-sub">carrier records it</text>

  <path d="M154 59 L166 59" class="d-arrow" />
  <polygon points="172,59 165,55.5 165,62.5" class="d-arrow-head" />

  <rect x="182" y="34" width="150" height="50" rx="8" class="d-box" />
  <text x="196" y="56" class="d-label">Audio stored</text>
  <text x="196" y="72" class="d-sub">recording uploaded</text>

  <path d="M336 59 L348 59" class="d-arrow" />
  <polygon points="354,59 347,55.5 347,62.5" class="d-arrow-head" />

  <rect x="364" y="34" width="150" height="50" rx="8" class="d-box-accent" />
  <text x="378" y="56" class="d-label">Record written</text>
  <text x="378" y="72" class="d-sub">answers OK regardless</text>

  <path d="M518 59 L530 59" class="d-arrow" />
  <polygon points="536,59 529,55.5 529,62.5" class="d-arrow-head" />

  <rect x="548" y="34" width="150" height="50" rx="8" class="d-box" />
  <text x="562" y="56" class="d-label">Call log</text>
  <text x="562" y="72" class="d-sub">reads the record</text>

  <text x="364" y="112" class="d-accent-text">a failure here is silent on both sides</text>
  <text x="0" y="150" class="d-sub">The carrier still has the call. The audio is still in storage. Only the record is missing,</text>
  <text x="0" y="168" class="d-sub">which is exactly why the call can be rebuilt afterwards.</text>
</svg>
</Figure>

## Spotting it

The tell is a mismatch between two places that should agree.

- **Your phone bill or carrier console shows calls the log does not.** This is
  the reliable check, and the one worth running after any change to voice.
- **Call volume drops to zero for a stretch** while the line demonstrably still
  answers. A quiet afternoon looks the same as a broken write; a quiet
  afternoon during which someone successfully called the number does not.
- **A call you made yourself is not there** a minute after you hung up. The log
  updates as calls end, so a test call that never appears is the fastest signal
  you have.

<Aside type="caution" title="Check after every voice change">
Place one test call and confirm it lands in the log. It takes under a minute
and it is the only check that exercises the whole path end to end. A healthy
dashboard, a connected line and a working greeting all stay green when the
record is the thing that failed.
</Aside>

## What can be recovered

The parts of a call live in different systems, and only some of them are ours.

| Part of the call | Where it lives | Recoverable |
| --- | --- | --- |
| Start time, duration, direction, numbers | The carrier's own call history | Yes |
| The recording | Object storage, written on hangup | Yes, if the upload finished |
| Transcript | Held in memory during the call | From the recording, without speaker labels |
| Outcome, sentiment, summary | Read from the transcript afterwards | Yes, once there is a transcript |
| Turn counts, response speed | Held in memory during the call | No |

The recording is what makes most of this possible. The audio is the call, so a
call whose audio survived can be transcribed after the fact, and everything read
from a transcript follows from there.

What the recording cannot give back is who said which sentence. Both sides are
mixed into a single track, so a transcript recovered this way is one block of
speech with no speaker labels. That is why the console shows it as **Both
speakers** and says where it came from, and why nothing offers to save a line
from it as a saved response: half of it is the caller's words.

Turn counts and response times are genuinely gone. They were measurements taken
while the call ran, not properties of the audio, and no amount of listening
recovers them.

## Rebuilding the record

Recovery is a deliberate, operator-run action, not something that happens
automatically. Automating it would mean continuously reconciling against the
carrier, and a reconciler that writes call records is a second writer for the
same table, which is how the two disagree.

The `voice-backfill` admin command does the work.

<Steps>

1. **Pull the calls from the carrier.** Their call history for the window is
   the authority: it has the start time, duration and dialled number for every
   call that actually connected.

2. **Write them into a file.** A JSON array, one object per call, using the
   call's provider-side identifier as `call_ref`:

   ```json
   [
     {
       "call_ref": "078E22C396144505.1786553777.1069053",
       "started_at": "2026-08-12T16:56:17Z",
       "duration_s": 17,
       "direction": "inbound",
       "from": "+15551234567",
       "to": "+19292240694",
       "disposition": "answered"
     }
   ]
   ```

   `call_ref` has to match what the platform would have used, or the rebuilt
   row will not line up with its recording. Everything except `call_ref` and
   `started_at` is optional.

3. **Dry run it.** Without `--apply` the command writes nothing and prints what
   it would do, including whether it found each call's recording in storage:

   ```
   admin voice-backfill --app <app> --file calls.json
   ```

4. **Apply.** Re-running is safe. A call that already has a record is skipped,
   never duplicated, so you can run the same file again after fixing one entry.

   ```
   admin voice-backfill --app <app> --file calls.json --apply
   ```

</Steps>

The command replays each call as real lifecycle events through the same
component that writes live calls. There is no second write path, so a recovered
row is built exactly the way a live one is.

## Reading a recovered call

Recovered rows are marked, because a record that quietly claims to be as
complete as a live one is worse than a missing record.

- They carry the **`recovered`** tag.
- They carry a note saying which carrier history they were rebuilt from and
  what is missing.
- The recording plays normally, if the audio survived.
- The transcript, once one has been recovered, is one **Both speakers** block
  with a line above it saying it came from the recording.
- Turn counts and response times are empty. That absence is itself the tell.

They sort into the log by when the **call** happened, not when the row was
written, so a call recovered a week later still appears on the day it was
made and counts on that day in the analytics.

The outcome, sentiment and summary arrive on their own within a few minutes of
the record existing: the same pass that classifies live calls picks up anything
finished and unclassified, whenever it happened. You do not run a second thing.

<Aside type="note" title="Recorded but unlinked audio">
The recording upload and the call record are separate writes. Audio can be
sitting in storage for a call that has no record, which is why the backfill
checks storage for each call and only attaches a recording it can actually
find. A call whose audio never uploaded is still worth recovering: you get the
call, without the tape.
</Aside>

## After a recovery

Recovering the calls is the smaller half. The write failed for a reason, and
until that reason is fixed every new call is being lost the same way.

<Steps>

1. **Place a test call and confirm it lands.** Recovery has told you nothing
   about whether the live path works again.

2. **Check the platform logs for rejected writes.** A failing record write logs
   a warning on every event; the silence is only on the wire.

3. **Re-run the backfill for the gap.** Once the live path is healthy, the
   window between the break and the fix is still empty and still recoverable.

</Steps>
