# Agent versions, publishing and rollback

> How the working copy differs from the published snapshot, what publishing captures, how rollback renumbers versions, and how to read the change list and diff.

A voice agent has two kinds of state: the **working copy** that the editor
writes, and an ordered history of **published snapshots**. Callers only ever
reach a published snapshot. Saving in the editor does not change what a caller
hears.

The Versions screen exists to show that split as a state rather than as a
toast, together with the two actions that change it: **Publish** and
**Restore**.

## The working copy and the published snapshot

The working copy lives in the agent's `agent_config` row. Every field in the
editor writes there — prompt, model, voice, turn detection, call flow, and the
rest. It has no version number.

Publishing copies the working copy into an immutable, numbered snapshot.
`agentVersions.status` reports the relationship between the two:

| `status` field  | Meaning                                                                    |
| --------------- | -------------------------------------------------------------------------- |
| `published`     | The snapshot callers are getting, including its `version_number`. Absent if the agent has never been published. |
| `unpublished`   | True when the working copy differs from the live snapshot.                 |
| `changed`       | The list of field names that differ between the working copy and the live snapshot. |
| `live`          | The definition the console treats as current, used as one side of the diff. |

The console renders these as two tags: `Live: v<n>` or **Never published**, and
**Unpublished changes** or **Up to date**. An agent with no `published`
snapshot cannot be reached by callers at all — there is nothing for a call to
resolve to.

## Publishing: what gets snapshotted and when callers see it

`agentVersions.publish` takes `{ orgId, agentId, note? }`. It snapshots the
**entire working copy definition**, not a field selection and not a diff — the
snapshot is self-contained, which is what makes restoring one later a complete
operation. The optional note is free text; the console's input caps it at 500
characters and sends nothing when it is left empty.

The response drives what the screen says:

| Response field | Console behaviour                                                                    |
| -------------- | ------------------------------------------------------------------------------------ |
| `version_number` | Reported as `Published v<n>. Callers are getting it now.`                          |
| `unchanged`    | Reported as `Nothing to publish — the live version already matches.` Publishing an unmodified working copy is therefore a no-op rather than an error. |
| `ready: false` | Treated as a failure state — see below.                                              |

The **Publish** button is only rendered while `status.unpublished` is true.
When the working copy matches the live snapshot there is nothing to publish and
the control is not offered.

After either mutation the console refetches both `status` and `list`, so the
history and the live tag update together.

## Rollback: republishing an older definition

`agentVersions.rollback` takes `{ orgId, agentId, versionId }`. Note that it
identifies the target by the history row's `id`, not by its version number.

Rollback does **not** move the version pointer backwards and does not reopen an
old number for writing. It republishes the older definition as a **new
snapshot at the next version number**. The response carries both numbers:

- `rolled_back_to` — the version whose definition was restored.
- `version_number` — the new version that now holds it.

The console reports this as `Rolled back to v<a>, published as v<b>.` Version
numbers only ever move forward, so the history stays a straight append-only
log and you can roll back a rollback.

In the history list, **Restore** only appears on rows whose `status` is not
`published`. There is no point restoring the snapshot that is already live.

A rollback can also come back with `ready: false`, for the same reason a
publish can.

## Reading the change list

`status.changed` and each history row's `changed` array contain raw field
names. The console maps them to the labels an operator recognises from the
editor:

| Field                            | Label                     |
| -------------------------------- | ------------------------- |
| `name`                           | Name                      |
| `system_prompt`                  | System prompt             |
| `welcome_message`                | Welcome message           |
| `llm_provider`                   | Model provider            |
| `llm_model`                      | Model                     |
| `asr_provider`                   | Speech-to-text provider   |
| `asr_model`                      | Speech-to-text model      |
| `tts_provider`                   | Voice provider            |
| `tts_voice`                      | Voice                     |
| `temperature`                    | Temperature               |
| `conversation_memory_messages`   | Conversation memory       |
| `pipeline_mode`                  | Pipeline                  |
| `turn_detection_type`            | Turn detection            |
| `turn_detection_threshold`       | Turn sensitivity          |
| `turn_detection_prefix_ms`       | Lead-in                   |
| `turn_detection_silence_ms`      | Silence to end turn       |
| `turn_detection_config`          | Turn tuning               |
| `noise_cancellation_model`       | Noise cancellation        |
| `background_audio_url`           | Background audio          |
| `language`                       | Language                  |
| `flow_graph`                     | Call flow                 |
| `external_handle`                | External agent            |

Any field name without a mapping is shown verbatim. On the live-status card the
list reads `Changed since v<n>: …`; on a history row it reads `Changed: …` for
that snapshot.

## Reading the side-by-side diff

The change list tells you *which* fields moved. **Compare** on a history row
tells you *how*. The console serialises both definitions to indented JSON and
runs a line-level longest-common-subsequence diff, then renders two columns:

- **Left**, red-tinted — lines present in the selected snapshot's `definition`
  and absent from the comparison target (deletions).
- **Right**, green-tinted — lines present only in the target (additions).
- Untinted lines are common to both. A greyed-out cell means that side has no
  line at that position.

The right-hand side is `status.live`, so a comparison is always "this snapshot
versus what the console reports as current". The header line above the diff
reads `No differences.` or `<n> changed lines`.

Line-level diffing is the reason this view beats the field list for prose
changes: a reworded system prompt shows up as one field name in `changed`, but
as the specific sentences that moved in the diff.

## Publishing an agent that cannot run yet: missing provider credentials

Publishing validates that a snapshot exists and is well-formed. It does **not**
require that every provider the snapshot names has usable credentials in your
org. Those are two separate concerns, and TeleQuick lets the publish
succeed so you are not blocked from versioning a configuration you are still
wiring up.

When the snapshot references a provider with no credential, `publish` and
`rollback` return `ready: false` together with `missing_providers`. The console
treats this as an error condition even though the mutation succeeded, and shows:

```
Published, but this agent cannot run yet — no credentials for <providers>.
Callers will hear an error until that is fixed.
```

If `missing_providers` is empty the provider list is omitted from the message.
The version is live either way: the pointer has moved, the history row exists,
and incoming calls will resolve to it and fail on the missing credential. Add
the credential for each named provider to clear the condition — the snapshot
itself does not need to be republished for the credential to take effect.

`ready: false` is reported for both mutations, so a rollback to an older
snapshot that names a provider you have since removed lands in the same state.

## Calls already in progress when you publish

Publishing swaps which snapshot is live. Be precise about what the screen
tells you and what it does not:

- `publish` returns the new `version_number`, `unchanged`, `ready` and
  `missing_providers`. It returns **no information about calls in progress**,
  and the console does not display any.
- `status` reports which snapshot is live and whether the working copy has
  drifted from it. It is a statement about the agent's configuration, not about
  any particular call.

So a green `Published v<n>. Callers are getting it now.` confirms that the
pointer moved — not that any specific connected call has been re-read. To
confirm end-to-end behaviour of a new version, place a fresh call after
publishing rather than inferring it from a call that was already up.

This is also the failure mode the screen was built to replace. Previously a
save wrote the row and a toast told the operator the agent was "still answering
with the old config" — a state, surfaced once, with no way to act on it. Now
that state is the **Unpublished changes** tag, and the action is the Publish
button next to it.

## The agentVersions API

All four procedures take `orgId` and `agentId`.

| Procedure  | Input                                  | Returns                                                                 |
| ---------- | -------------------------------------- | ----------------------------------------------------------------------- |
| `status`   | `{ orgId, agentId }`                   | `{ published?: { version_number }, unpublished, changed[], live }`      |
| `list`     | `{ orgId, agentId, limit }`            | Rows of `{ id, version_number, status, note, changed[], definition, created_at, published_at }` |
| `publish`  | `{ orgId, agentId, note? }`            | `{ version_number, unchanged?, ready?, missing_providers? }`            |
| `rollback` | `{ orgId, agentId, versionId }`        | `{ rolled_back_to, version_number, ready?, missing_providers? }`        |

Notes on `list`: rows are the history, newest first as rendered. `status` is
`published` for the live row and something else for every other row — the
console keys the **Live** tag and the availability of **Restore** off that
single value. `published_at` is preferred over `created_at` for the displayed
timestamp. `definition` is the full snapshot, which is what makes client-side
diffing possible without a second round trip. The console requests
`limit: 30`.
