Skip to content

Move an alias safely

Point a name at a new object without losing a concurrent change.

An alias is a name in the store that points at an object, outside the object’s identity. Every request that moves an alias says what it expects the alias to hold now. If someone moved it after you looked, your request is refused instead of applied.

Before you start

  • A running server. In the commands, $SOCK is its socket.
  • The ID of the new target: an object accepted in this store, with a role it has if you name one.

Steps

  1. Read what the alias holds now:

    Shell
    lema --socket $SOCK call find --params '{"prefix": "app.main"}'

    find returns every alias whose name starts with the prefix, each as {id[, role], generation}:

    Output
    {"app.main": {"generation": 2, "id": "<old id>"}}
  2. Move it, and pass that entry back as expect:

    Shell
    lema --socket $SOCK call alias --params '{
      "name": "app.main",
      "target": {"id": "<new id>"},
      "expect": {"id": "<old id>", "generation": 2},
      "idempotency_key": "agent-7/app.main/3"
    }'

    The response is {name, id[, role], generation, changed}, here at generation 3 with changed: true. The move is one line in the store’s history.jsonl, and it makes no native call.

  3. If the response is lost, send the identical request again. With the same key and the same request, the store returns the first response and applies nothing. Without a key, the retry would be refused as stale, because the alias already names the new target.

  4. If the move is refused as stale, the alias changed after you read it. Go back to step 1, decide whether your move still makes sense, and send it with the new expectation.

Choose the expectation

expectThe move applies when
nullthe name does not exist yet
{id[, role]}the alias names that target, at any generation
{id[, role], generation}the alias names that target at exactly that generation

An alias is created at generation 1, and each move adds one. Aliases are never removed, so a generation is never reused. That is why only the third form catches a move to another object and back: the alias names the old object again, but at a later generation. Re-asserting the target the alias already has moves nothing and keeps its generation.

To apply a change whatever the alias holds, give "overwrite": true instead of expect. The history records it as an overwrite. There is no separate rollback: moving an alias back is a move like any other, with its own expectation.

To move an implementation’s alias only with a checked theorem behind it, use replace instead (Replace an implementation).

Aliases on submit and publish

submit and import take aliases, mapping each name to {target, expect} or {target, overwrite: true}. publish takes aliases, mapping each draft name (such as LemaDraft.Json.decode) to {expect} or {overwrite: true}, and the target is that name’s object in the draft. Both check every expectation before any native call, so a stale one is found at no cost. The response adds aliases, with {id[, role], generation, changed} for each name.

A request’s alias changes apply together or not at all, as one history line. submit, import, publish and replace take idempotency_key in the same way as alias.

Refusals

Message (start)Meaning
alias app.main: no expected target: give expect …The request has neither expect nor overwrite.
alias app.main: give expect or overwrite, not bothThe request has both.
stale expectation: alias app.main was expected to be new, but it names …expect: null for a name that exists.
stale expectation: alias app.main was expected to name …, but it names …Another target is there now.
stale expectation: alias app.main names … at generation 3, not at the expected generation 1 (it has moved since, and back)The alias moved away and back.
stale expectation: alias app.main was expected to name …, but it does not exist (expect null to create it)The name is new.
alias target … is not acceptedThe target is not accepted in this store.
idempotency key "…" was used for another request …That key belongs to a different request.

Idempotency keys are 1 to 256 bytes without control characters. They never expire and are shared by every client of the store, so start yours with a prefix of your own, such as your actor ID.

Written against lema-lang at commit 5a94951, 17 Oct 2026.