<!-- Generated by crates/swarmops-cli/src/reference.rs from the code: do not edit. Change the code, then run SWARMOPS_REGEN_REFERENCE=1 with the regen_reference test. -->

# swarmops op undo

Move every ref operation OP moved back to what it named before, all or none.

Each ref must still name what the operation set it to (or what it named before, which is skipped): one that was moved since stops the undo with E\_CAS\_MISMATCH, and nothing is written. An undo run again is a replay (already\_applied). Changesets are never removed.

```yaml
title: "swarmops op undo"
description: "Move every ref operation OP moved back to what it named before, all or none."
group: "operations"
since: "0.1.0"
mutates: true
identity_required: true
mcp_tool: "undo"
envelope_op: "undo"
exit_codes: [0, 1, 6]
errors: [E_NOTHING_TO_UNDO, E_OP_NOT_FOUND]
syntax: "swarmops op undo [OPTIONS] <OP>"
```

## Syntax

```text
swarmops op undo [OPTIONS] <OP>
```

## Required arguments

### `OP`

Syntax: `<OP>`.

The operation, as op log numbers it.

## Optional arguments

The command also takes the [global options](../cli.md#global-options).

## Examples

### CLI

```sh
swarmops --repo [tmp]/store --as agent/worker-17 op undo 3
```

```json
{
  "ok": true,
  "op": "undo",
  "api": 1,
  "result": {
    "undone": 3,
    "op": 4,
    "ref_moves": [
      {
        "name": "main",
        "old": "[c2]",
        "new": "[c1]",
        "actor": "agent/worker-17",
        "at": "[at]"
      }
    ],
    "already_applied": false
  },
  "meta": {
    "repo": "[tmp]/store",
    "elapsed_ms": "[elapsed_ms]",
    "resolved": {},
    "auth": "trust"
  }
}
```

Exit code 0. Recorded by the CLI tests. A name in square brackets, such as `[tmp]` or `[c1]`, stands for what differs from run to run: the test's temporary directory, an id, a time.

### MCP

No recorded example yet.

## Details

`op undo` takes back the ref moves of one operation: every ref that operation `OP` moved goes
back to what it named before `OP`, in one transaction of its own, all of them or none. It moves
refs only. Changesets are never removed, so whatever an undo takes off a ref stays in the store,
readable by id. Use it to take back a mistaken `ref set`, a publish or a land that should not
have reached `main`, a deletion or a creation of a ref.

### What it restores

For each ref, `OP`'s net move counts: from the first `old` the operation recorded for the ref to
the last `new`. A ref the operation moved and moved back is left alone. The undo then moves each
ref, in the order of `OP`'s moves, from `OP`'s `new` back to its `old`:

| What `OP` did to the ref | What the undo does |
| --- | --- |
| Moved it from `A` to `B` (`ref set`, a publish, a land) | Moves it from `B` back to `A` |
| Created it at `B` | Deletes it |
| Deleted it, from `A` | Creates it again at `A`, with `version` 1 |

### When it refuses, and when it is a replay

The undo reads operation `OP` under the store's write lock, after the write has checked the
journal's tail, then compares each ref:

| The ref names now | Result |
| --- | --- |
| What `OP` set it to (`B`; no ref, if `OP` deleted it) | Moved back |
| What it named before `OP` (`A`; no ref, if `OP` created it) | Skipped: an earlier undo, or a later move, put it there already |
| Anything else | The whole undo is refused with `E_CAS_MISMATCH` (exit 4) and nothing is written, the other refs' moves included |

With every ref skipped, nothing is written either: the result says `already_applied: true`, has
no `op`, and lists no moves. So running the same undo again is safe.

Before the comparison:

- **`E_OP_NOT_FOUND`** (exit 6): the store has no operation `OP`. `details.last` is the newest
  operation's number, `null` for a store without one.
- **`E_NOTHING_TO_UNDO`** (exit 1): `OP` moved no ref (a `commit` without `--advance`, a
  compaction), or moved each ref back to where it found it.
- **`E_IDENTITY_REQUIRED`** (exit 5): an undo is a write, so it needs `--as` or
  `SWARMOPS_AGENT`.

### After a refusal

`E_CAS_MISMATCH` here carries `details.name` (the ref), `details.actual` (what it names now,
`null` when it does not exist) and `details.op`; the hint is `swarmops op log`, which only reads.
A later operation moved the ref, so the message gives two ways on:

- **Undo the later operations first, newest first, then `OP` again.** Each undo is checked the
  same way, so this works only while nothing else moved the ref meanwhile.
- **Keep the later moves, and land a change that reverses `OP`'s.** No command builds the
  reverse change for you
  ([Known limitations](../../known-limitations.md#commits-refs-and-history)).

The retry flag says that the undo can succeed once the ref is back where `OP` left it. Running
the same undo again without changing anything fails the same way.

### The undo is an operation

A successful undo records one operation of kind `ref_move`, its number in the result's `op`. Its
moves name the undoer as `actor` and carry the undo's time. Another `op undo` of that number
takes the undo back, which moves the refs forward again.

### Patterns

Take back the newest move of `main`:

```sh
OP=$(swarmops op log | jq -r '[.result.ops[] | select(any(.ref_moves[]; .name == "main"))][0].op')
swarmops op log --before $((OP + 1)) -n 1 | jq -c '.result.ops[0]'
swarmops op undo "$OP"
```

The second line lists the operation once more, to check that its moves are the ones you mean
before you undo it (below).

Read the result: `already_applied: true` means nothing was written because every ref was back
already; otherwise `ref_moves` lists each move made and `op` is the undo's own number.

### Caveats

- **Check the number first.** `op log` lists an operation before its sync has finished. If the
  store takes that frame back (a failed sync, a power loss), the next write's frame gets the same
  number, and an undo of the number you read moves back that other operation's refs, with no
  error, as long as they hold what it set. List the operation again before you undo it, above
  all after an error ([Known limitations](../../known-limitations.md#commits-refs-and-history)).
- **It is not a revert.** An undo moves refs; it does not record a change that reverses the
  files. After it, the changesets `OP` added are on no ref, yet still in the store.
- **Land keys.** A keyed land looks for an earlier land of its key on the target's
  first-parent chain. A land that an undo took off `main` is no longer there, so the same
  `land --key` run again lands once more (`status: landed`, not `replayed`).
- **Durability and the lock** work as for [`ref set`](../commands/ref-set.md#after-the-move):
  the undo commits when its frame is durable, `warnings` reports a later step that failed, and a
  lock wait past 10 s is `E_BUSY`.

### Through MCP

The MCP tool is `undo`, with `op`. Its description says the same: a ref moved since stops it,
undo the later operations first, and a second run is a replay.

### Guides

- [Reading and repairing history: Undo an operation](../../guides/history-and-undo.md#undo-an-operation)
- [Reading and repairing history: Recover from a refusal](../../guides/history-and-undo.md#recover-from-a-refusal)
- [Concepts: Op log and undo](../../concepts.md#op-log-and-undo)

## Return information

`result` is a `UndoResp` (`swarmops schema UndoResp` prints its JSON Schema).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `already_applied` | `boolean` | yes | Every ref the operation moved already names what it named before, so nothing was written: an undo run again is a replay, and safe. |
| `op` | `integer or null` | no | The operation this undo recorded, which another undo can take; omitted when it wrote nothing (`already_applied`). |
| `ref_moves` | `array of RefMoveOut` | yes | The moves it made: each ref from what the operation set back to what it named before. |
| `undone` | `integer` | yes | The operation undone. |
| `warnings` | `array of string` | no | Problems found after the undo became durable, such as failed housekeeping; the undo itself succeeded. Omitted when empty. |

Envelope `op`: `undo`.

Exit codes:

- `0`: Success.
- `1`: The operation failed.
- `6`: Something the request names does not exist.

## Errors

- [`E_NOTHING_TO_UNDO`](../errors.md#e_nothing_to_undo) (exit 1): The operation moved no ref, so there is nothing to restore.
- [`E_OP_NOT_FOUND`](../errors.md#e_op_not_found) (exit 6): The store has no operation by that number.

Any command that opens a store may also fail with a store error, such as [`E_NOT_A_REPO`](../errors.md#e_not_a_repo), [`E_IO`](../errors.md#e_io) or [`E_STORE_CORRUPT`](../errors.md#e_store_corrupt); a command line the grammar refuses is [`E_INVALID_ARG`](../errors.md#e_invalid_arg) (exit 2).

## See also

- [`swarmops op log`](op-log.md)
- [CLI reference](../cli.md)
- [Error codes](../errors.md)
