---
title: "Coordinación evitable"
description: "Los teoremas CALM e invariant confluence dicen cuándo un cerrojo es necesario y cuándo es folclore heredado. Se aplican, uno por uno, a los tres puntos donde ALLAN serializa: el cerrojo de workspace del enjambre, el SELECT FOR UPDATE de la cartera y el -p 1 de los tests."
allan.topic: foundation
allan.service: allan-server
allan.date: 08/04/2026
allan.fuente: [Platform Server/internal/agents/swarm_loop_v3.go, Platform Server/internal/mcp/protocol.go, Platform Server/internal/mcp/builtin.go, Platform Server/internal/todo/todo.go, Platform Server/internal/app/tenant_mcp.go, Platform Server/internal/app/dfly_test_helper_test.go, Platform Server/cmd/allan-server/dispatcher_test.go, Platform Server/cmd/allan/main_test.go, Platform Server/internal/app/activity_events_integration_test.go, Platform Server/internal/responses/outbox_dfly.go, Platform Server/internal/dfly/keys.go, Economics/economics/services/wallet.py, Economics/economics/db/models.py, Economics/tests/conftest.py, docs/arquitectura/swarm-v3-execution-contract-redesign.md, docs/economia/economics-implementation-plan.md, docs/arquitectura/event-triggering-implementation-plan.md]
allan.audiencia: [desarrollador]
allan.madurez: emergente
---

Un cerrojo casi nunca se escribe: se hereda. Alguien vio dos escrituras al mismo
sitio, puso un mutex, y a partir de ahí el mutex se convirtió en parte del
paisaje. Nadie lo quita porque nadie sabe qué se rompería, y no lo sabe porque
en ningún sitio está escrito qué propiedad protege. La consecuencia es un
sistema que paga latencia y pierde paralelismo por una razón que ya no consta.

Hay dos resultados que convierten esa pregunta en una decisión con respuesta.
El primero es el teorema CALM, que traslada la pregunta del almacenamiento al
programa: «A program has a consistent, coordination-free distributed
implementation if and only if it is monotonic» (Hellerstein y Alvaro, 2019). El
segundo es la *invariant confluence* de Bailis y colaboradores, que hace lo
mismo desde la base de datos y con la ventaja de operar sobre invariantes que un
desarrollador puede enunciar: «A globally I-valid system can execute a set of
transactions T with coordination-freedom, transactional availability,
convergence if and only if T is ℐ-confluent with respect to I» (Bailis et al.,
2015).

La tesis de este ensayo es la lectura práctica de ambos: **la necesidad de
coordinar no es una propiedad del dato, sino del invariante que se quiere
mantener sobre él**. Dos escrituras concurrentes al mismo sitio pueden ser
inofensivas; una escritura a un sitio y una lectura de otro pueden no serlo. La
pregunta correcta nunca es «¿escriben los dos aquí?», sino «¿qué afirmación deja
de ser cierta si estos dos avanzan sin mirarse?». Cuando esa afirmación no
existe, el cerrojo es folclore y se puede borrar.

Conviene enunciar también los límites, porque las dos fuentes los enuncian
solas. CALM «falls short of being a constructive result—it does not actually
tell us how to write consistent, coordination-free distributed systems»: dice
cuándo se puede, no cómo. Y la I-confluencia va acompañada de una advertencia
que corta de raíz la lectura triunfalista: «simply because these invariants are
ℐ-confluent does not mean that all execution strategies will scale well: for
example, using locking would not be coordination-free». Ser I-confluente
habilita una implementación sin coordinación; no la escribe.

ALLAN serializa en tres sitios visibles. Se examinan aquí uno por uno con el
mismo método: nombrar el invariante, clasificarlo, y concluir.

## El cerrojo de workspace del enjambre

El motor `swarm_v3` mantiene un gestor de cerrojos por ejecución
(`swarm_loop_v3.go:2611-2614`) construido dentro de `Run`
(`swarm_loop_v3.go:415`). Antes de ejecutar una tool, el llamante pide
`lockFor(writeLocks, mutates)`. Si `mutates` es falso no se toma nada; si es
cierto y no hay claves declaradas, se toma una única clave literal:
`keys = []string{"workspace"}` (`swarm_loop_v3.go:2625-2627`). Las claves se
ordenan globalmente antes de adquirirse (`normalizedLockKeys`,
`swarm_loop_v3.go:2649-2662`), lo que hace imposible el interbloqueo por orden
inverso de adquisición. Esa parte está bien y no se discute.

Lo que se discute es el predicado. `mutates` lo decide `toolMutates`
(`swarm_loop_v3.go:2664-2676`): serializa todo lo que no esté anotado como
solo-lectura por su servidor. El índice se construye en
`readOnlyToolIndexFor` a partir de `Tool.ReadOnly()`, que es exactamente
`t.Annotations != nil && t.Annotations.ReadOnlyHint` (`mcp/protocol.go:41-43`).

Ese predicado divide el mundo en «lee» y «escribe». CALM lo divide en otro
sitio: en si la conclusión de una rama puede ser *retractada* por otra. Escribir
no es el problema; retractar lo es. En álgebra relacional, la línea la traza la
diferencia de conjuntos: «we can allow each machine to employ selection,
projection, intersection, join and transitive closure (the monotonic operators
of relational algebra), but not set-difference (the sole non-monotonic
operator)». Añadir un hecho es monótono aunque sea una escritura; borrarlo o
sobrescribirlo no lo es.

Y resulta que el propio protocolo MCP publica el eje correcto. En la definición
de `ToolAnnotations`, `destructiveHint` significa: «If true, the tool may
perform destructive updates to its environment. If false, the tool performs only
additive updates.» *Additive updates* es, literalmente, monotonía. ALLAN declara
ese campo en su struct (`mcp/protocol.go:33`) y **no lo lee en ninguna parte**:
la única aparición fuera de la declaración es la tool `mail/sendEmail`, que lo
fija a `true` (`mcp/builtin.go:362-364`). El campo que responde a la pregunta
está parseado y muerto; el que se usa responde a otra pregunta.

El coste se puede contar. De las 19 tools nativas del motor, 9 no llevan
anotación alguna (`edit/createDirectory`, `edit/createFile`, `edit/editFiles`,
`execute/runInTerminal`, `todo/create`, `todo/update`, `todo/delete`,
`agent/create`, `agent/generate`), y por tanto las 9 serializan, más
`mail/sendEmail`. Con conectores de terceros la proporción no se ha medido, pero
solo puede ir en la misma dirección: la anotación es opcional y su ausencia,
según el criterio del motor, equivale a «serializa».

Aplicando el criterio una a una:

`todo/create` inserta un elemento en un conjunto con identificador generado
localmente: `NewID` mezcla un nonce de `crypto/rand` con SHA-256
(`todo/todo.go:60-65`). Eso es exactamente el caso que la tabla 2 de Bailis
clasifica como «Uniqueness (choose some value) → Yes»: si el sistema elige el
valor, la unicidad se mantiene sin coordinación. Dos `todo/create` concurrentes
conmutan y no pueden invalidarse. Serializarlos es folclore. Demostrable.

`edit/createFile` es el caso opuesto y por poco: su descripción dice «Fails if
the file already exists» (`mcp/builtin.go:313`). Es una inserción bajo un
invariante de unicidad *con valor elegido por el cliente*, la fila que Bailis
marca «Uniqueness (choose specific value) → No». Necesita coordinación de
verdad. Pero la necesita **sobre esa ruta**, no sobre el workspace entero: dos
`createFile` a rutas distintas son tan independientes como dos `todo/create`.

`edit/editFiles` («Create or replace», `mcp/builtin.go:321`) es una
sobreescritura ciega: retracta el contenido anterior. No monótona, sin matices.
`execute/runInTerminal` tiene efecto arbitrario y desconocido, y ahí el
conservadurismo es la única respuesta honesta —además coincide con el default
del propio protocolo, que es `destructiveHint: true`.

De ahí sale la primera conclusión, y es de las que quitan código: el predicado
de serialización debe ser `destructiveHint != false` en lugar de
`readOnlyHint != true`, y la clave del cerrojo debe salir de los *argumentos* de
la llamada, no de su nombre. La monotonía no es una propiedad de la tool: es una
propiedad de la tool aplicada a un argumento. El cambio es una relajación
estricta —los no anotados siguen serializando, porque el default del protocolo
los da por destructivos— y libera exactamente a los servidores que se molestan
en declarar que solo añaden.

Falta la parte incómoda. El gestor de cerrojos es un `map[string]*sync.Mutex`
creado por invocación de `Run` (`swarm_loop_v3.go:415`), es decir, en memoria y
por ejecución. No excluye a otra ejecución del mismo proceso, ni a otra réplica,
ni a un run anidado: `tenant.agent/delegate` no lleva anotaciones
(`app/tenant_mcp.go:46`), luego cuenta como mutante, luego el llamante retiene
`"workspace"` durante toda la delegación síncrona mientras el run anidado —con
*su propio* gestor de cerrojos— hace lo que quiera sobre el mismo directorio.
Dentro de un run, además, el root nunca compite con sus workers: `runSpawnBatch`
lanza el lote y espera en `wg.Wait()` antes de devolver el control
(`swarm_loop_v3.go:1526`), de modo que la contención real se da únicamente entre
workers del mismo lote.

Un cerrojo que se llama `"workspace"` y no protege el workspace es peor que no
tenerlo: induce a razonar como si el recurso estuviera protegido. Y la
maquinaria para hacerlo bien está escrita a medias. `swarmV3SpawnSpec` lleva
`WriteLocks` (`swarm_loop_v3.go:239`), el prompt del spawn se lo anuncia al
modelo —«Declare write_locks for any shared resource the worker may mutate;
workers holding overlapping locks serialize» (`swarm_loop_v3.go:2270`)— y
`FlowStepIR` también lo declara (`flows/types.go:93`). Pero el rediseño del
contrato de ejecución constata que «`write_locks` existe en `FlowStepIR`, pero
no llega automáticamente al `swarmV3SpawnSpec` que gobierna los locks reales del
worker» y lo clasifica como «Concurrencia mutante sin la serialización
declarada» (`swarm-v3-execution-contract-redesign.md:182-184` y `:277`). Hoy las
únicas claves efectivas son las que el modelo se inventa en su spawn.

## El SELECT FOR UPDATE de la cartera

En Economics, cada mutación de la cartera pasa por `_lock_state`, que toma la
fila `(tenant, period)` con `.with_for_update()` (`wallet.py:181-188`). Hay cinco
llamantes: `reserve`, `extend`, `settle`, `release` y `record_platform_work`
(`wallet.py:406`, `:475`, `:541`, `:564`, `:583`). El plan de implementación lo
enuncia como el corazón de la transacción de apertura de misión
(`economics-implementation-plan.md:217-226`), y el modelo lo repite en el
docstring de la tabla: «This is THE row that every mission opening locks»
(`db/models.py:86`).

Aquí la teoría dice que sí, y decirlo es tan parte del ejercicio como decir que
no. El invariante de `reserve` es un techo: `consumed + reserved ≤ granted`,
comprobado en `wallet.py:409` y `:420`, y el efecto es un incremento
(`wallet.py:427`). En la tabla 2 de Bailis eso es la fila «< (Counter) |
Increment | No», y el texto lo enuncia sin ambigüedad: «a row-level
"greater-than" (>) threshold invariant is ℐ-confluent for counter increment and
assign (←) but not decrement (Claims 11, 13), while a row-level "less-than" (<)
threshold invariant is ℐ-confluent for counter decrement and assign but not
increment». Dos reservas concurrentes que localmente caben pueden no caber
juntas; su merge es inválido. El cerrojo no es folclore: es el caso canónico.

Ahora, la precisión importa, porque `reserve` y `extend` son dos de los cinco
llamantes. Los otros tres no están del mismo lado.

`release` decrementa `reserved` (`wallet.py:565`): un decremento bajo un techo
`<` es I-confluente por la misma frase citada. `settle` mueve `reserved` a
`consumed` (`wallet.py:542-543`) y no comprueba nada: cuando el consumo real
supera lo retenido, lo debita igualmente y registra el exceso en
`overshoot_wu_milli`, decisión documentada y deliberada (`wallet.py:508-517`).
Esto tiene una consecuencia que conviene enunciar en voz alta: **el sistema no
mantiene `consumed + reserved ≤ granted` como invariante global**. Lo prueba la
existencia de `overage_wu_milli`, que es precisamente el reporte de los estados
donde esa desigualdad se ha roto (`wallet.py:162-172`). Lo que el cerrojo
protege no es el techo, sino un enunciado más estrecho: *ninguna reserva se
admite si no cabe en el momento de admitirla*.

Lo que `settle` y `release` sí necesitan es que la transición de estado de la
reserva ocurra a lo sumo una vez —`held → settled | released`, comprobada hoy en
Python leyendo `reservation.status` (`wallet.py:527-530`, `:561-562`)—. Eso es
un CAS sobre la fila de la reserva, no un cerrojo sobre la fila de la cartera.
Con el guardián ganado, el ajuste de los contadores es un delta conmutativo que
no requiere leer el estado anterior: `UPDATE wallet_state SET reserved =
reserved - :held, consumed = consumed + :billable`. La diferencia es medible,
porque Postgres documenta la duración: «FOR UPDATE causes the rows retrieved by
the SELECT statement to be locked as though for update. This prevents them from
being locked, modified or deleted by other transactions until the current
transaction ends.» Hoy la fila caliente del tenant queda retenida desde el
`SELECT` hasta el `COMMIT`, incluyendo la agregación de grants de
`_live_grants_wu_milli` (`wallet.py:211-217`) que ni `settle` ni `release`
necesitan. En un tenant activo, con retención de 1 WC por turno según el propio
docstring (`wallet.py:508-509`), esa es la ruta más transitada del producto.

El tercero es más simple: `record_platform_work` incrementa `platform_wu_milli`,
un contador que —dice el comentario— «Never affects the balance»
(`wallet.py:576`). Un contador que no participa de ningún invariante admite
cualquier operación sin coordinación; es el extremo trivial que la propia
literatura nombra: «with no invariants, a user can safely perform any operations
she likes». Toma el cerrojo de tenant sin comprarse nada con él. Y hay un
agravante verificable: en todo el repositorio de Economics la función solo se
invoca desde un test (`tests/test_wallet.py:197`). Ningún camino de producción
escribe `wallet_state.platform_wu_milli`, y sin embargo cuatro respuestas de API
lo publican (`routers/wallet.py:42`, `routers/missions.py:223`,
`routers/admin.py:759`, y con otro origen `routers/ledger.py:92`).

Queda la pregunta interesante: ¿se puede quitar coordinación de `reserve`, que
es donde de verdad hace falta? La respuesta clásica es el escrow, y su versión
moderna son los *bounded counters*: «Our approach adapts ideas from escrow
transactions to devise a solution that is decentralized, fault-tolerant and
fast» (Balegas et al., 2015). La idea es repartir el margen del techo en cuotas
locales; mientras la cuota alcanza, la operación es local. ALLAN ya tiene la
forma: `personal_minimum_remaining_wu_milli` entra en `reserve` y se materializa
en `floor_hold_wu_milli` (`wallet.py:416-417`, `:441`), y la cascada de topes lo
calcula fuera del cerrojo (`ceilings.py:600-652`). Lo que no tiene es la
partición: `floor_consumed_wu_milli` es un único número por tenant
(`wallet.py:544`), de modo que el gasto contra el mínimo de cada persona sigue
pasando por la misma fila.

Conviene no vender esto como gratis, y menos porque el diseño ya lo pensó y
decidió lo contrario: derivar el mínimo por persona honestamente «significaría
sumar `min(spent_i, minimum_i)` sobre cada miembro en la ruta de autorización de
cada turno», y ese barrido «es un check que se borra la primera vez que aparece
en una gráfica de latencia» (`db/models.py:107-113`). La réplica posible es que
un contador por miembro no es un barrido —el total comprometido depende del plan
y del censo, que cambian rara vez, y puede materializarse—, pero eso es una
propuesta, no un hecho, y quien la implemente hereda la carga de la prueba.
También conviene recordar el límite: el escrow **no elimina** la coordinación,
la localiza; el invariante `gasto_i ≤ mínimo_i` sigue siendo un techo bajo
incrementos, solo que el conjunto de transacciones en conflicto pasa a ser el de
una persona en vez del de un tenant.

Un último apunte de honestidad sobre esta sección: la fila lleva un campo
`version` que se incrementa en cuatro sitios (`wallet.py:428`, `:490`, `:545`,
`:566`) y no se lee jamás en ninguna condición. Es un testigo de concurrencia
optimista instalado y desconectado —justo la alternativa que permitiría cambiar
el cerrojo pesimista por un reintento— y hoy es peso muerto que confunde.

## El `-p 1` de los tests

El tercer punto no está en el motor sino en cómo se prueba. Los planes de diseño
lo consagran como regla de trabajo: «Tests contra Dragonfly real: ejecutar
paquetes combinados con `-p 1` antes de culpar a un cambio (falsos rojos por
carrera de `FlushDB` entre paquetes son conocidos)»
(`event-triggering-implementation-plan.md:17-19`), y otro diseño lo repite
(`chat-narration-design.md:297-298`). La documentación de Go describe la
bandera: «-p n · the number of programs, such as build commands or test
binaries, that can be run in parallel. The default is GOMAXPROCS, normally the
number of CPUs available.» Es decir: la regla renuncia al paralelismo de toda la
suite.

La causa está en dos ficheros. `internal/app/dfly_test_helper_test.go` y
`cmd/allan-server/dispatcher_test.go` definen cada uno su `newTestDflyClient`,
ambos con `testDB := 15` por defecto (`:33` y `:78`) y ambos con un `FlushDB` al
inicio de cada test (`:52` y `:96`). Son unos 69 y unos 26 puntos de llamada
respectivamente contra la misma base lógica. Como `go test` lanza un binario por
paquete en paralelo, el `FlushDB` del paquete B borra el estado del test en
vuelo del paquete A.

El invariante aquí es «cada test observa únicamente lo que él mismo escribió», y
el mecanismo elegido para garantizarlo es un borrado. `FlushDB` es diferencia de
conjuntos: el único operador no monótono. La conclusión de CALM es inmediata y
no admite negociación: mientras el aislamiento se construya por retracción, el
paralelismo entre paquetes exige coordinación, y `-p 1` es la coordinación más
burda disponible.

El caso concreto se puede leer entero. En
`internal/app/activity_events_integration_test.go`, el test invoca
`PeekOutboxDragonfly(ctx, client, 50)` —que lee el stream *global* del outbox,
sin prefijo de tenant (`responses/outbox_dfly.go:100-101`)— y afirma
`if len(got) != 3` (`:49`). Una cuenta exacta sobre una colección compartida es
una consulta no monótona: cualquier escritura ajena la invalida. El comentario
del propio test lo dice sin darse cuenta de lo que está diciendo:
«newTestDflyClient flushes the whole test Dragonfly before this runs, so a plain
session id is enough to isolate this test's events from any other test's
leftovers on the shared outbox stream» (`:24-26`).

La versión monótona ya existe, en el mismo repositorio, con su justificación
escrita: `cmd/allan/main_test.go:46-48` —«Never FLUSHDB here… This test uses
unique run/workspace ids and needs no global cleanup»—. Escribir con
identificadores únicos y afirmar por contención en vez de por igualdad hace que
el conjunto de claves solo crezca, que ninguna conclusión de un test pueda ser
retractada por otro, y que `-p 1` deje de tener función. En el caso citado la
reescritura es de dos líneas: los registros que devuelve `PeekOutboxDragonfly`
llevan `RunID` y `SessionID`, así que basta filtrar por el propio y afirmar
sobre el filtrado.

Tres costes, porque no es gratis. El primero: el aislamiento por tenant llega
hasta donde llega el prefijo `t:{tenant}:` (`dfly/keys.go:50-55`), y hay media
docena larga de colecciones globales que no lo llevan por diseño
(`dfly/keys.go:70-72`) —`approvals:expiry`, `bill:records`, `bill:budgets`,
`forge:sessions:by-created`, `mem:intake`, `http-runs`—. Un test que barra una
de ellas seguirá siendo no monótono y seguirá necesitando o coordinación o una
clave particionada. El segundo: sin `FlushDB` la basura se acumula, y el
almacén no tiene hoy política de retención sobre `run:*` ni sobre sus trazas.
El tercero: la reescritura de las ~70 aserciones de longitud exacta del paquete
`internal/app` no es mecánica.

Aun así, la aritmética es favorable. Se cambia una regla que serializa la suite
entera, para siempre, por un trabajo acotado de una vez.

## Invariantes, enunciados de forma falsable

Cuatro afirmaciones que este ensayo sostiene y que se pueden refutar leyendo el
código o midiendo:

Toda tool sin `readOnlyHint` serializa hoy tras la clave `"workspace"`, y esa
clave es un `sync.Mutex` en memoria creado por invocación de `Run`: no excluye a
otro run del mismo proceso ni a otra réplica. Refutable exhibiendo un mecanismo
de exclusión entre runs sobre `/workspace`.

Ningún camino de producción de Economics escribe
`wallet_state.platform_wu_milli`; su único escritor
(`wallet.record_platform_work`) toma el cerrojo de tenant y solo lo llama un
test. Refutable exhibiendo un llamante en producción.

El invariante `consumed + reserved ≤ granted` no se mantiene: `settle` lo viola
por diseño y `overage_wu_milli` existe para reportarlo. Refutable exhibiendo la
comprobación en la ruta de liquidación.

Con `FlushDB` eliminado de los dos helpers y las aserciones filtradas por
identificador propio, la suite pasa en verde sin `-p 1`. Es la más falsable de
las cuatro: se comprueba ejecutándola.

## Lo que está roto, sin terminar o sin evidencia

`write_locks` es un campo decorativo en la ruta de flows: declarado en
`FlowStepIR` (`flows/types.go:93`), anunciado al modelo en el esquema del spawn
(`swarm_loop_v3.go:2281`) y sin consumidor que lo lleve al `swarmV3SpawnSpec`.
El propio corpus de diseño lo tipifica como defecto de propagación de runtime.

`destructiveHint` se declara (`mcp/protocol.go:33`), se emite una vez
(`mcp/builtin.go:363`) y no se lee nunca. Toda la argumentación de la primera
sección depende de que ese campo empiece a leerse; hasta entonces es una
propuesta.

La coordinación de la cartera no tiene prueba que la ejercite. El test que se
llama `test_concurrent_missions_cannot_spend_the_same_capacity_twice`
(`tests/test_wallet.py:135-153`) hace tres reservas **en serie**, y el propio
`conftest` lo reconoce: «`SELECT ... FOR UPDATE` is a no-op on SQLite, so a
genuine two-connection race cannot be reproduced» (`tests/conftest.py:6-8`). Lo
que se verifica es la aritmética, no la exclusión. La afirmación de que el
cerrojo funciona descansa en un E2E de Docker que este ensayo no ha ejecutado.

No hay medición de contención en ninguno de los tres puntos. Ni tiempo de espera
en el mutex de `swarm_loop_v3`, ni tiempo de retención de la fila de
`wallet_state`, ni duración comparada de la suite con y sin `-p 1`. Las
conclusiones de este texto son sobre corrección —qué cerrojo es necesario— y no
sobre rendimiento; cuánto cuesta cada uno es una pregunta abierta que se
responde instrumentando, no razonando.

Y una limitación de método, la que las propias fuentes imponen: la I-confluencia
solo es tan buena como el invariante que se le da. «If invariants are
incorrectly or incompletely specified, an ℐ-confluent database system may
violate application-level correctness.» En un motor agéntico el invariante más
importante —qué significa que el trabajo esté bien hecho— no es una restricción
de integridad y no se puede enunciar así. Ningún teorema decide por el diseñador
si es aceptable que dos workers escriban el mismo informe en orden arbitrario.
Lo que estos dos sí hacen es eliminar la excusa de no haber mirado.

## Referencias

**Hellerstein, J. M. y Alvaro, P. (2019).** *Keeping CALM: When Distributed
Consistency is Easy*. arXiv:1901.01930. https://arxiv.org/abs/1901.01930 —
«The CALM Theorem shows that the programs that have consistent, coordination-free
distributed implementations are exactly the programs that can be expressed in
monotonic logic.» · «A program has a consistent, coordination-free distributed
implementation if and only if it is monotonic.» · «A program P is *monotonic* if
for any input sets S,T where S⊆T, P(S)⊆P(T).» · «In the relational algebra, we
can allow each machine to employ selection, projection, intersection, join and
transitive closure (the monotonic operators of relational algebra), but not
set-difference (the sole non-monotonic operator).» · «The key insight in CALM is
to focus on consistency from the viewpoint of program outcomes rather than the
traditional histories of storage mutation.» · «CALM falls short of being a
constructive result—it does not actually tell us how to write consistent,
coordination-free distributed systems.» · «The CALM conjecture was presented in a
keynote talk at PODS 2010 and written up shortly thereafter alongside a number of
corollaries. In a subsequent series of papers, Ameloot and colleagues presented a
formalization and proof of the CALM Theorem which remains the reference formalism
at this time.» — *Procedencia de cada frase, corregida: solo la primera está en
el resumen de arXiv. Las seis restantes proceden del texto completo recuperado
vía ar5iv (https://ar5iv.labs.arxiv.org/html/1901.01930), donde «A program has a
consistent, coordination-free distributed implementation if and only if it is
monotonic» es el enunciado del Teorema 1 y «A program P is monotonic…» el de la
Definición 1. Una nota anterior atribuía las dos primeras frases al resumen; era
inexacta para la segunda. La versión de CACM (2020) devolvió HTTP 403 y no se ha
usado.*

**Bailis, P., Fekete, A., Franklin, M. J., Ghodsi, A., Hellerstein, J. M. y
Stoica, I. (2015).** *Coordination Avoidance in Database Systems*. PVLDB 8(3),
185-196. https://www.vldb.org/pvldb/vol8/p185-bailis.pdf — «Definition 6
(ℐ-confluence). A set of transactions T is ℐ-confluent with respect to invariant
I if, for all I-T-reachable states Di, Dj with a common ancestor state, Di ⊔ Dj
is I-valid.» · «Theorem 1. A globally I-valid system can execute a set of
transactions T with coordination-freedom, transactional availability,
convergence if and only if T is ℐ-confluent with respect to I.» · «coordination
can only be avoided if all local commit decisions are globally valid.» · «a
row-level "greater-than" (>) threshold invariant is ℐ-confluent for counter
increment and assign (←) but not decrement (Claims 11, 13), while a row-level
"less-than" (<) threshold invariant is ℐ-confluent for counter decrement and
assign but not increment (Claims 12, 14).» · Tabla 2, filas «< | Increment
[Counter] | No» y «Uniqueness | Choose specific value | No» frente a «Uniqueness
| Choose some value | Yes». · «At one extreme, if a user's transactions do not
modify database state, she can guarantee any satisfiable invariant. At the other
extreme, with no invariants, a user can safely perform any operations she
likes.» · «simply because these invariants are ℐ-confluent does not mean that all
execution strategies will scale well: for example, using locking would not be
coordination-free.» · «If invariants are incorrectly or incompletely specified,
an ℐ-confluent database system may violate application-level correctness.» —
*Citas leídas directamente del PDF de VLDB, páginas 188-192.*

**Balegas, V., Serra, D., Duarte, S., Ferreira, C., Rodrigues, R., Preguiça, N.,
Shapiro, M. y Najafzadeh, M. (2015).** *Extending Eventually Consistent Cloud
Databases for Enforcing Numeric Invariants*. SRDS 2015. arXiv:1503.09052.
https://arxiv.org/abs/1503.09052 — «We present a new replicated data type,
called bounded counter, which adds support for numeric invariants to eventually
consistent geo-replicated databases.» · «Our approach adapts ideas from escrow
transactions to devise a solution that is decentralized, fault-tolerant and
fast.» — *Recuperado el resumen; el texto completo del artículo no se ha leído,
de modo que aquí solo se usa para nombrar la técnica, no para respaldar ninguna
cifra.*

**Model Context Protocol (2025).** *Schema 2025-06-18, interfaz
`ToolAnnotations`*.
https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-06-18/schema.ts
— «If true, the tool does not modify its environment. Default: false»
(`readOnlyHint`) · «If true, the tool may perform destructive updates to its
environment. If false, the tool performs only additive updates. (This property is
meaningful only when `readOnlyHint == false`) Default: true» (`destructiveHint`)
· «NOTE: all properties in ToolAnnotations are **hints**. They are not
guaranteed to provide a faithful description of tool behavior… Clients should
never make tool use decisions based on ToolAnnotations received from untrusted
servers.»

**Model Context Protocol (2025).** *Specification 2025-06-18 — Server / Tools*.
https://modelcontextprotocol.io/specification/2025-06-18/server/tools — «For
trust & safety and security, clients **MUST** consider tool annotations to be
untrusted unless they come from trusted servers.» — *Relevante porque la política
de cerrojos de ALLAN descansa por completo en una anotación que la especificación
declara no fiable.*

**The Go Authors.** *Command go — Compile packages and dependencies*.
https://pkg.go.dev/cmd/go — «-p n · the number of programs, such as build
commands or test binaries, that can be run in parallel. The default is
GOMAXPROCS, normally the number of CPUs available.»

**The PostgreSQL Global Development Group.** *PostgreSQL 16 Documentation,
13.3.2. Row-Level Locks*. https://www.postgresql.org/docs/16/explicit-locking.html
— «FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as
though for update. This prevents them from being locked, modified or deleted by
other transactions until the current transaction ends.» · «Row-level locks are
released at transaction end or during savepoint rollback, just like table-level
locks.» · «Row-level locks do not affect data querying; they block only *writers
and lockers* to the same row.»
