Fundamentos
Fundamento / Coordinación evitable

Coordinación evitable

Fundamento·08/04/2026·Tiempo de lectura: 22 minutos·allan-server

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.»

¿Le ha resultado útil esta página?

Enviar comentarios

Este artículo se publica desde allan-docs. El mismo texto se sirve a los agentes a través del servidor MCP.