Address destinations by id and batch-local handles in the effect vocabulary
Supersedes the rejection of ids in ADR-0071.
Decision
Name destinations by id in every effect. A destination created earlier in the same batch has no id yet, and the model must not invent one. Ids derive from the creating modification so that replay reproduces them (ADR-0049), and deriving them is the server's job. The effect that creates the destination declares a handle local to the batch, such as new:vienna, and later effects in that batch name the handle. Before anything is stored, the server replaces every handle with the real derived id. Placement works the same way, after a named neighbour or at the front, and never at a numbered position.
The unresolved and stored forms of an effect are separate types. Only the unresolved form can hold a handle, and resolution converts one into the other, so storing an unresolved handle fails to compile.
Positions survive only where an outside caller supplies one, such as an MCP tool argument, and the server turns each into a neighbour before building an effect.
Why
The model writes a batch of effects in one response, against the snapshot it was shown. With positional names the batch contradicted itself. Any effect that changes the list shifts every later position in the same batch, so a later effect, resolved against the numbering the model saw, lands on a different destination from the one it meant. No other writer is involved, so no concurrency check can see it.
ADR-0071 had kept positions because the model reasons in them and can't name something that doesn't exist yet. Tests against the real model showed it refers to existing destinations by id at least as reliably as by position, and the position grammar was seen retargeting after an insert with no error. Handles cover what doesn't exist yet. The grammar gives placement a single production, because a shorter sibling shape measurably pulled the model toward it whatever was asked.
Rejected alternatives
- Keeping positions and widening the concurrency check. The check would have to reason about effects inside one batch, and it would still be a runtime check between a wrong state we can represent and the database.
- Letting the model create ids. That hands id authority to an untrusted author and breaks the derivation replay depends on.
- One type for both forms, checked when applied. The check confused an unresolved handle with "front of the list" and put things at the front without complaint.
Consequences
A handle names one thing and nothing renumbers it, so a batch can't retarget itself, and the type system enforces that on every write path. Every effect the model can write exists in two forms, with conversion code that grows with the vocabulary.
A field's wire name is part of the contract between the grammar and the unresolved form, and serde by default drops an unknown field without complaint. The unresolved form rejects unknown fields, and a test checks every literal the grammar accepts against the variant it names (ADR-0032).