↓ Skip to main content
  1. Blog/

Minix: a hybrid agent that does not need to send every question to the cloud

·30 mins· loading
Carles Abarca
Author
Carles Abarca
Writing about AI, digital transformation, and the forces reshaping technology.

Recently, the pace of innovation in open-weight AI models has been frenetic, with promises of “free, unlimited AI” running on consumer hardware. The specifications are becoming remarkable: a MacBook Pro with M4 Max can offer up to 128 GB of unified memory and 546 GB/s of memory bandwidth, while a Mac Studio with M3 Ultra reaches 512 GB and exceeds 800 GB/s. Apple says the latter can run language models with more than 600 billion parameters locally.

The evolution of the models themselves matters just as much. Mixture of Experts (MoE) architectures make it possible to build enormous models while activating only a fraction of their parameters for each inference. Qwen3-30B-A3B, for example, has 30 billion parameters but activates approximately 3 billion per token. More efficient models, quantization, and hardware with large amounts of unified memory are rapidly narrowing the gap between AI available in large data centers and what can run privately and locally on personal machines.

Yet a narrower gap does not mean that cloud-based frontier models are no longer necessary. When a task demands complex reasoning or advanced agentic capabilities, or when latency and throughput—TTFT (time to first token) and inference speed in tokens per second—are decisive factors, cloud infrastructure and frontier models still offer important advantages.

Meanwhile, YouTube channels devoted to local AI are multiplying, and debates between local AI fanboys and frontier AI fanboys keep going without a clear winner: they are arguing about different things.

From my perspective, the optimal approach is hybrid AI: combining local and cloud models to optimize costs, provide fallback mechanisms that reduce dependence on a single provider, and offer an alternative when handling information I would rather not send outside my own systems.

That vision led to Minix. It is a working experiment that I already use as my personal assistant: I maintain a single conversation, and the system decides, for each request, where it makes the most sense to run the intelligence.

I do not want to choose a model every time I type. I want a short translation handled locally and to turn to the cloud when I need more demanding reasoning. And I want to know which is happening.

Minix’s server: a Mac mini M4 Pro with 24 GB
#

My LiteLLM server is a Mac mini with an Apple M4 Pro and 24 GB of unified memory. It is the entry point to the models: it receives Minix’s requests, selects a route, and connects the agent to local inference or the cloud provider. Local inference is served through LM Studio. I do not need a server farm to experiment with this architecture, but I do need to respect the available memory and avoid loading two large models simultaneously.

This Mac is a shared dependency: if the cloud fails, the local route can remain available; if the LiteLLM server itself stops, having two inference destinations does not guarantee continuity. Model fallback and gateway high availability are different problems.

One conversation, three LLMs, and a router
#

I talk to Minix through Matrix. Hermes maintains the history and agent loop: it receives my message, calls the model, executes tools when needed, and returns the answer. LiteLLM provides a common entry point, minix-smart, behind which the complexity router operates.

minix-smart is not an LLM. It is a routing alias. Understanding the system means separating the models from the components that transport, classify, and route their calls.

FunctionAlias or componentConfigured modelRole in the experiment
Classify requests locallyminix-classifierqwen35-08b-classifier-benchSix decisions, one per turn
Generate local answersminix-local, served by LM Studiogoogle/gemma-4-12bT1, T3, and T5
Generate cloud answersminix-cloudchatgpt/gpt-5.5T2, T4, and T6
Handle the REASONING categoryminix-thinkchatgpt/gpt-5.5Configured route, not used in these six turns

These are three distinct models, not four: minix-cloud and minix-think point to the same cloud model name. SIMPLE and MEDIUM go to minix-local; COMPLEX goes to minix-cloud; REASONING goes to minix-think. The default when classification fails is minix-cloud: I must not confuse that policy with local recovery from a cloud outage. Session affinity is disabled, so a cloud answer does not lock the entire conversation to that destination.

I use the current aliases here. The historical experiment and its appendices retain the earlier names: kathy-smart → minix-smart, kathy-local → minix-local, kathy-classifier → minix-classifier, kathy-premium → minix-cloud, and kathy-codex → minix-think. The sixth alias, minix-smart-mlx, is an existing auxiliary alias, not an additional route in this experiment. minix-think retains reasoning_effort: high, which was already configured before migration. The table maps current functions to historical turns, not to a new run.

Minix architecture: local Qwen classifier, local Gemma, and cloud GPT-5.5; planned extensions in pink

The drawing shows the configured flow, including the REASONING route that did not occur in the experiment. Planned privacy is separate and dashed pink; deployed fallback is green. Turn and call labels refer to the historical benchmark. I have web and file tools, plus a Docker terminal without network access; that does not make web tools local or establish isolation of the whole agent. These six turns invoked no tools or auxiliary vision model; I do not add a fourth LLM without evidence.

Mermaid source · HTML version

Editable diagram source
flowchart TB
  M["Matrix<br/>My conversation"]
  H["Hermes · Minix<br/>History + tool loop"]
  T["Web · files · terminal<br/>Docker: terminal without network<br/>0 calls in the experiment"]
  R["LiteLLM · minix-smart<br/>Router / alias, NOT an LLM<br/>No session affinity"]
  Q["minix-classifier · local LLM<br/>qwen35-08b-classifier-bench<br/>6 historical calls"]
  L["minix-local · LM Studio<br/>google/gemma-4-12b<br/>Local LLM · T1, T3, T5"]
  P["minix-cloud<br/>COMPLEX · T2, T4, T6<br/>Also classifier default"]
  C["minix-think<br/>REASONING · effort high<br/>Not used in the six turns"]
  G["LLM cloud · chatgpt/gpt-5.5<br/>Same model in both aliases"]
  O["Answer + route metadata<br/>Hermes → Matrix · local / cloud<br/>Not an audit of auxiliary calls"]
  S["PLANNED local substitution<br/>History + text + tool results<br/>Identity mapping stays local"]
  K["Cloud with placeholders<br/>Only placeholder-bearing text<br/>PLANNED flow"]
  F["DEPLOYED reactive fallback<br/>smart / cloud / think → local · max 1 hop<br/>500 / 429: 1 retry · 400 / 401 / timeout: 0<br/>Cooldown: 60 s after 2 failures"]
  M --> H
  H --> T
  T --> H
  H --> R
  R --> Q
  Q --> |"tier"| R
  R --> |"SIMPLE / MEDIUM"| L
  R --> |"COMPLEX / default"| P
  R --> |"REASONING"| C
  P --> G
  C --> G
  L --> O
  G --> O
  O --> |"tool loop"| H
  S -.-> K
  V["Planned LOCAL restoration<br/>Validate markers + restore<br/>Private mapping stays local"]
  D["Local health every 300 s · no cloud probes: passive recovery<br/>Local terminal: error on failure · no return to cloud<br/>No global deadline · health filter may fail open when all destinations are unhealthy"]
  K -.-> V
  S -.-> V
  R --> |"fallback"| F
  P --> |"fallback"| F
  C --> |"fallback"| F
  F --> L
  classDef planned fill:#2b1725,stroke:#fb7185,stroke-dasharray:5 5,color:#ffffff
  class S,K,V planned
  classDef deployed fill:#064e3b,stroke:#34d399,color:#ffffff
  class F,D deployed

An answer carries a marker: 🖥️ for local generation and ☁️ for cloud generation. The system adds it from metadata about the served route; it is not the model describing itself. It identifies the final generation, not every auxiliary call in a tool-using task.

I checked the names and mappings against the configuration and experiment artifacts. Logs directly corroborate Gemma and the classifier. The hybrid run did not archive each cloud response’s raw header: the marker and its rendering code support the cloud category, while the configured mapping supports GPT-5.5. The baseline did preserve all six kathy-premium deployment headers. That identifies the deployment, not an independent fingerprint of the provider’s model weights.

The experiment: returning to local after using the cloud
#

I prepared six requests about one project, Puente, a file synchronizer between two servers that may work while disconnected. I fixed the prompts before starting, without requesting a particular route or adjusting the classifier during the test. I preserved the existing conversation, including its earlier history.

The sequence was local → cloud → local → cloud → local → cloud:

  1. T1, local: a welcome sentence and three alternative names. Gemma offered Nexo, Vínculo, and Nodo.
  2. T2, cloud: metadata, merge rules, pseudocode, tombstones, and convergence analysis. The answer proposed causality and concurrent variants, but not a complete proof of the algorithm.
  3. T3, local: literal extraction of three values. It returned nombre=Puente, nodos=A,B, and política=conservar conflictos, exactly as requested.
  4. T4, cloud: a concrete edit/delete conflict. The answer preserved variants (2,1) and (1,2), summarized their context as (2,2), and explained a later resolution (3,2).
  5. T5, local: a short English translation: “Puente preserves concurrent edits and requires explicit resolution of conflicts.”
  6. T6, cloud: a critical review of three dangerous shortcuts: choosing content by hash, collecting tombstones too early, and reusing an identity with its counter reset to zero.

The interesting part is not just that a difficult question reaches the cloud. It is that the next simple question returns to local execution while retaining the conversation. I do not need another chat or to move the context myself.

To make the result reviewable beyond my summary, I include all six prompts and all twelve complete answers, hybrid and cloud, at the end of this article. The source-language text is preserved verbatim, including its mistakes; I have not reconstructed or corrected it to make the experiment look better. The appendix has English editorial guidance, not translated experimental dialogue.

Each run produced six final answers, with no tool calls or observed retries. Both handled the simple tasks satisfactorily. The complex answers offered useful ideas but retained gaps around general conflict-set merging and safe version collection. My review was not blind and does not establish statistical quality equivalence: choosing a cloud model does not turn its answer into a formal proof.

What I measured, and what 48% means
#

Historical benchmark, before the alias migration and new fallback policy. I retain the original measurements unchanged; they are not a rerun of the current configuration.

I recorded input and output tokens, including each call’s context. I then repeated the same six prompts in an all-cloud run, accumulating that run’s own answers rather than the hybrid answers.

TurnHybrid routeLocal answer-call tokensHybrid cloud tokensAll-cloud baseline tokens
T1local7,35108,257
T2cloud09,3569,368
T3local8,65009,441
T4cloud010,28410,301
T5local9,631010,379
T6cloud011,31611,454
Total25,63230,95659,200

These totals include input and output, not just visible text. The baseline adds up to 56,550 input + 2,650 output; the hybrid cloud portion is 28,510 + 2,446. The classifier consumed another 2,014 local tokens and about 1.71 seconds in aggregate, separate from answer-generation calls.

The observed difference is:

(59,200 − 30,956) / 59,200 × 100 = 47.71%: 28,244 fewer cloud tokens.

I have not converted the 25,632 local tokens into supposed cloud-token savings: I compare cloud usage across two actual runs. Nor have I eliminated that work; I moved some of it to local infrastructure. The hybrid’s 14,336 cached cloud tokens and the baseline’s 15,360 are already included in input, not counted twice. Different tokenizers prevent treating combined local/cloud counts as equivalent computation.

This is an indicative comparison, not a controlled causal or universal savings estimate. I recovered the available initial history and tools, but not every original dynamic instruction exactly. The hybrid ran through Matrix and the baseline through the API; their answers and caches evolved differently. One six-turn conversation does not measure the classifier’s general accuracy either.

The defensible claim is: in this experiment, Minix used approximately 48% fewer cloud tokens than a comparable all-cloud run. That does not mean a 48% lower bill or greater speed: I have not calculated monetary cost, electricity, hardware, or maintenance, or run a controlled latency benchmark.

Continuity: reactive fallback is now deployed
#

I deployed a bounded LiteLLM policy: minix-cloud, minix-think, and minix-smart can fall back to minix-local, with at most one fallback hop. Local is terminal: if it also fails, the request ends with an error, without returning to cloud or entering a cycle. This is not a promise that Gemma can solve every task equally well.

For cloud HTTP 500 and 429 errors I allow one retry; for 400, 401, and timeouts, zero retries. Disabling retries does not mean disabling fallback. After the second failure, the deployment enters a 60-second cooldown. I retain the existing inference timeouts; there is no strict global deadline for the entire sequence.

I check local health every 300 seconds. I do not run background cloud probes: cloud recovery is passive, when cooldown expires and a new request tries the route again. If all destinations are marked unhealthy, the health filter can allow another attempt (fail-open); that does not change the terminal error when local ultimately fails. Both cloud aliases share a provider and model: they are not independent redundancy.

Verification separates failure tests from live use. I have 28 passing unit tests and earlier evidence from 12 isolated loopback cases, not rerun during this migration. I did not force a production outage. In production I verified streaming HTTP 200 responses from local, cloud, and think without fallback, with content, completion, and exact served-route identifiers; I also tested smart and confirmed an actual final local reply in Matrix. This validates those paths, not every API mode.

One specific limitation remains: the non-streaming cloud call returns Unknown items in responses API response: []. I reproduced the same failure with the previous configuration; I do not attribute it to the migration or claim it is fixed. The streaming path passed verification.

Inference fallback does not imply semantic task reassessment, an automatic degraded-mode notice, or demonstrated protection against repeating tools with external effects. I do not claim to have validated those behaviors. Accessing an unavailable external service does not become possible just because the model changes. Runtime, storage, and communication must remain available: local inference does not guarantee that a remote chat survives a lost connection.

Privacy: send the problem, not necessarily the identities
#

The other decision I want to add concerns data rather than difficulty: what information needs to leave my environment to solve the task?

The planned mechanism would locally replace sensitive data with consistent identifiers, keeping the mapping under local control, with restricted access and a lifetime limited to the task. For example, my fictional request:

“Draft an email to Elena at Taller Norte about Project Brisa.”

would become, before leaving:

“Draft an email to [PERSON_1] at [COMPANY_1] about [PROJECT_1].”

The cloud would write using those markers. On return, the local layer would validate each identifier and restore the original values for me. The same person would retain the same marker within the task; an unknown or altered marker would not authorize invented names or retrieval of a different mapping.

Cleaning the latest message alone would not be enough. I want the check to cover everything leaving: history, documents, tool results, and relevant metadata. An attachment that cannot be handled safely should stay local or be excluded. If solving the task requires revealing an identity, the alternative would be to keep it local, request my specific authorization, or explain the limitation—not silently send the original.

This is reversible pseudonymization, not guaranteed anonymity. Context can reveal identities even after names disappear; detection and markers can fail too. That is why output validation, logs without secrets, and conservative handling of uncertain cases belong in the behavior I want.

My assistant, not a contest between models
#

I do not use Minix to prove that local models always replace cloud models, or the reverse. I use it to choose where each task runs without having to manage that choice manually.

The experiment already lets me observe something concrete: a conversation that returns to local execution after using the cloud, with lower cloud consumption in the comparison I ran. Reactive fallback is now deployed; data control remains the next step, without erasing the limits of what I measured.

The question stops being “which model is the most powerful?” and becomes something more useful:

Which part of this task really needs to leave my environment?


Complete conversations: the six turns
#

I preserve the six original prompts and all twelve complete answers here: one hybrid and one all-cloud answer per turn. Each run accumulated its own answers; I place them together for review, not as a single conversation.

The blocks are verbatim in their original language (Spanish, except for the English translations requested in T5). This is not an English translation of the experiment. I retain even plural wording and questionable claims inside quoted material; they are not my editorial voice. Experiment identifiers are part of the prompts. Hybrid-run icons are system-added metadata, not model-generated words. I exclude internal instructions, earlier history, Matrix identifiers, and credentials.

Critical reading: both runs handle the simple tasks satisfactorily. T2 does not prove a general merge over conflict sets; T4 works through one example rather than establishing that proof. T6 identifies useful risks and properties but does not establish minimal garbage-collection conditions. In the baseline, rejecting old dominated operations must not be confused with discarding legitimate concurrent edits. I ran neither a distributed synchronizer nor a blind quality evaluation here.

Contents
#

T1 · T2 · T3 · T4 · T5 · T6

T1
#

My original prompt
#

[MXHYB-20260926-A T1] Vamos a diseñar juntos un pequeño sincronizador de archivos entre dos servidores que pueden trabajar desconectados, A y B. Lo llamaremos Puente. De momento, responde en español con una bienvenida de una frase y tres nombres alternativos cortos. No crees archivos ni ejecutes nada.

Hybrid answer — local
#

🖥️ Bienvenido al diseño del proyecto Puente para la sincronización entre servidores.

Nombres alternativos: Nexo, Vínculo, Nodo.

All-cloud answer
#

Bienvenido a Puente: podemos diseñar un sincronizador simple, robusto y preparado para desconexiones entre A y B.

- Nexo
- Brisa
- Trama

T2
#

My original prompt
#

[MXHYB-20260926-A T2] Sigamos con Puente. A y B pueden editar o borrar el mismo archivo mientras están desconectados; los relojes físicos no son fiables y la red duplica y reordena mensajes. Diseña un algoritmo de sincronización que converja sin perder silenciosamente ediciones concurrentes. Especifica metadatos, reglas de fusión, tombstones y condiciones para recolectarlos. Da pseudocódigo y justifica convergencia e idempotencia; incluye un contraejemplo a elegir siempre la fecha más reciente. Unas 450 palabras, sin ejecutar ni crear archivos.

Hybrid answer — cloud
#

☁️ Un diseño seguro para Puente debe tratar cada archivo como un objeto replicado con historial causal, no como “el último gana”.

**Metadatos por archivo**
- `id`: ruta lógica o identificador estable.
- `replica_id`: `A` o `B`.
- `version_vector`: mapa `{replica_id: contador}` que resume las actualizaciones conocidas.
- `dot`: par único `(replica_id, contador)` de la operación local.
- `content_hash`: hash del contenido, si existe.
- `deleted`: booleano.
- `tombstone`: registro de borrado con `dot`, `version_vector` y fecha local solo informativa.
- `conflicts`: conjunto de variantes concurrentes, cada una con contenido y metadatos causales.

Los relojes físicos no deciden nada. Solo sirven para mostrar información al usuario.

**Reglas**
Cada edición local incrementa el contador propio y crea una nueva versión causal. Cada borrado crea un tombstone, no elimina inmediatamente el registro. Al sincronizar, se comparan vectores:

- Si `remote.version_vector <= local.version_vector`, el remoto ya está incluido: ignorar.
- Si `local.version_vector <= remote.version_vector`, aceptar remoto.
- Si ninguno domina al otro, hay concurrencia:
  - edición vs edición: conservar ambas variantes en `conflicts`, o intentar fusión automática si el tipo lo permite; si la fusión no es limpia, exponer conflicto.
  - edición vs borrado: no borrar silenciosamente; conservar la edición como conflicto contra tombstone.
  - borrado vs borrado: fusionar tombstones tomando la unión causal.

**Pseudocódigo**

```text
edit(file, content):
  clock[self] += 1
  dot = (self, clock[self])
  file.content = content
  file.deleted = false
  file.version_vector[self] = clock[self]
  file.dot = dot
  file.conflicts = {}

delete(file):
  clock[self] += 1
  dot = (self, clock[self])
  file.content = null
  file.deleted = true
  file.tombstone = {dot, version_vector + self counter}
  file.version_vector[self] = clock[self]

merge(local, remote):
  if dominates(local.vv, remote.vv):
    return local

  if dominates(remote.vv, local.vv):
    return remote

  merged.vv = pointwise_max(local.vv, remote.vv)

  if local.deleted and remote.deleted:
    merged.deleted = true
    merged.tombstone = merge_tombstones(local, remote)
    return merged

  if not local.deleted and not remote.deleted:
    result = try_semantic_merge(local.content, remote.content)
    if result.clean:
      merged.content = result.content
      merged.conflicts = {}
    else:
      merged.content = null
      merged.conflicts = {variant(local), variant(remote)}
    return merged

  merged.deleted = false
  merged.content = null
  merged.conflicts = {variant(local), variant(remote)}
  return merged
```

**Tombstones y recolección**
Un tombstone solo puede eliminarse cuando todas las réplicas conocidas han observado causalmente el borrado. Con dos servidores, A puede recolectar un tombstone de B cuando sabe que B ha recibido un vector que domina el vector del tombstone, y viceversa. En sistemas ampliables, hace falta un mecanismo de membresía estable o acknowledgements por réplica. Si hay réplicas desconocidas o restauraciones desde copias antiguas, no se debe recolectar aún.

**Convergencia e idempotencia**
La convergencia viene de usar una unión determinista: máximo punto a punto de vectores, preservación de variantes concurrentes y reglas simétricas. Si A y B reciben los mismos estados, acaban calculando el mismo resultado. La idempotencia se obtiene porque reentregar el mismo mensaje no cambia nada: un vector ya dominado se ignora, y los conflictos se identifican por `dot` o hash causal, no por número de recepción.

**Contraejemplo al “más reciente gana”**
A edita `informe.txt` a “versión legal” con reloj adelantado a las 12:10. B, desconectado, edita a “versión financiera” con reloj atrasado a las 12:05. Ambas ediciones son concurrentes y válidas. Elegir la fecha más reciente descarta silenciosamente la versión financiera, aunque no fue sobrescrita causalmente por A.

All-cloud answer
#

Propongo que Puente use versiones causales, no fechas físicas. Cada servidor replica un conjunto de objetos CRDT por ruta lógica, con detección explícita de conflictos.

**Metadatos por archivo**

- `node_id`: `A` o `B`.
- `version_vector`: mapa `{nodo: contador}` que identifica causalidad.
- `content_hash`: hash del contenido.
- `op_id`: par único `(node_id, contador)`.
- `deleted`: booleano.
- `tombstone`: si está borrado, guarda `delete_vector`, `delete_op_id` y fecha local solo informativa.
- `conflicts`: conjunto de variantes concurrentes, cada una con contenido/hash/vector.

Cada operación local incrementa el contador del nodo. Una edición genera una versión con `deleted=false`; un borrado genera una tombstone con `deleted=true`.

**Reglas de sincronización**

Al recibir un estado remoto para una ruta:

```pseudo
function dominates(v1, v2):
    return forall n: v1[n] >= v2[n] and exists n: v1[n] > v2[n]

function concurrent(v1, v2):
    return not dominates(v1, v2) and not dominates(v2, v1)

function merge(local, remote):
    if remote.op_id already_seen:
        return local

    mark_seen(remote.op_id)

    if dominates(remote.vector, local.vector):
        return remote

    if dominates(local.vector, remote.vector):
        return local

    if concurrent(local.vector, remote.vector):
        return resolve_concurrent(local, remote)
```

La resolución concurrente no debe descartar silenciosamente. Para archivos binarios o cambios incompatibles:

```pseudo
function resolve_concurrent(x, y):
    if x.deleted and y.deleted:
        return tombstone(union_vector(x.vector, y.vector))

    if x.deleted and not y.deleted:
        return conflict_record(
            visible = y,
            conflicts = [tombstone_variant(x)]
        )

    if not x.deleted and y.deleted:
        return conflict_record(
            visible = x,
            conflicts = [tombstone_variant(y)]
        )

    if same content_hash:
        return version(content=x.content, vector=union_vector(x.vector, y.vector))

    if textual_auto_merge_possible(x.content, y.content, base):
        return merged_file(
            content=three_way_merge(base, x, y),
            vector=union_vector(x.vector, y.vector),
            conflicts=[]
        )

    return conflict_record(
        visible = deterministic_choice(x, y),   // solo para presentación
        conflicts = [x, y],
        vector = union_vector(x.vector, y.vector)
    )
```

En disco, un conflicto puede exponerse como `archivo`, `archivo.conflict-A`, `archivo.conflict-B` o como un manifiesto. Lo importante: ambas ediciones quedan accesibles.

**Tombstones**

Una tombstone representa “esta ruta fue borrada en esta causalidad”. Debe propagarse igual que una edición para evitar que una réplica antigua resucite el archivo. Puede recolectarse solo cuando todos los nodos conocidos han observado el borrado:

```pseudo
function can_gc_tombstone(t):
    return forall node in membership:
        ack_vector[node] >= t.delete_vector[node]
```

Si la membresía cambia, hace falta un protocolo de épocas: no se recolectan tombstones de una época hasta que todos los miembros de esa época hayan acusado recibo, o hasta que exista una instantánea completa posterior aceptada por todos los miembros activos.

**Convergencia e idempotencia**

Converge porque la fusión es determinista, conmutativa y asociativa sobre vectores causales y conjuntos de variantes. Los mensajes duplicados no cambian el resultado porque `op_id` y los hashes hacen la aplicación idempotente. El reordenamiento no importa: si llega primero una versión antigua, luego será dominada; si llegan versiones concurrentes, se conservan ambas.

**Contraejemplo a “fecha más reciente”**

A y B editan `nota.txt` desconectados. A escribe “precio 10” a las 10:05, pero su reloj va adelantado; B escribe “precio 12” a las 10:01 reales, con reloj atrasado marcando 09:50. Al reconectar, elegir la fecha más reciente conserva A y borra B, aunque son cambios concurrentes y ambos deben revisarse. Peor aún: con relojes desviados, el resultado puede depender del reloj defectuoso, no de la causalidad.

T3
#

My original prompt
#

[MXHYB-20260926-A T3] Para la ficha de Puente, extrae literalmente estos tres valores del texto, sin explicaciones ni consultar nada: «nombre=Puente; nodos=A,B; política=conservar conflictos». Devuelve solo tres líneas con formato clave=valor.

Hybrid answer — local
#

🖥️ nombre=Puente
nodos=A,B
política=conservar conflictos

All-cloud answer
#

nombre=Puente
nodos=A,B
política=conservar conflictos

T4
#

My original prompt
#

[MXHYB-20260926-A T4] Probemos el diseño de Puente con este historial. Parten del mismo archivo con vector (1,1). Desconectados, A lo edita y genera (2,1), B lo borra y genera (1,2). A recibe primero el borrado, luego un duplicado del archivo inicial; B recibe primero la edición y luego un duplicado del borrado. Calcula el estado final de ambos y demuestra qué propiedades debe cumplir la fusión para coincidir. Explica cómo resolver explícitamente el conflicto con una nueva versión y por qué borrar el tombstone demasiado pronto puede resucitar datos. Señala cualquier ambigüedad de tu propuesta anterior y corrígela. Unas 350 palabras; razonamiento en el chat, sin herramientas ni archivos.

Hybrid answer — cloud
#

☁️ Estado inicial: archivo existente con vector `(1,1)`. Luego hay dos operaciones concurrentes: edición de A con vector `(2,1)` y borrado de B con vector `(1,2)`. Ninguna domina a la otra, porque A tiene más información de A y B más información de B.

**Estado final esperado**
- En A, al recibir el borrado `(1,2)`, la fusión detecta concurrencia edición-vs-borrado y debe producir un conflicto explícito: variante editada de A `(2,1)` frente a tombstone de B `(1,2)`, con vector fusionado `(2,2)`.
- El duplicado del archivo inicial `(1,1)` queda dominado por `(2,2)`, así que se ignora.
- En B, al recibir la edición `(2,1)`, obtiene el mismo conflicto edición-vs-borrado con vector `(2,2)`.
- El duplicado del borrado `(1,2)` queda dominado por `(2,2)` o ya está incluido como variante/tombstone, así que no cambia nada.

Por tanto, ambos terminan con un estado conflictivo convergente: “archivo no resuelto”, con la edición de A preservada y el borrado de B preservado como intención concurrente. No debe aparecer como archivo normal ni como borrado definitivo hasta que alguien resuelva.

**Propiedades necesarias de la fusión**
La fusión debe ser:
- conmutativa: `merge(edición, borrado) = merge(borrado, edición)`;
- asociativa: el orden de llegada no cambia el resultado final;
- idempotente: duplicados como `(1,1)` o `(1,2)` no alteran el estado;
- inflacionaria causalmente: no pierde dots ni conocimiento causal;
- determinista: las mismas entradas producen el mismo estado.

**Resolución explícita**
Un usuario o regla superior debe crear una nueva operación, por ejemplo en A: “conservar edición y descartar borrado”. Esa resolución incrementa A y genera `(3,2)`, dominando tanto `(2,1)` como `(1,2)`. El nuevo estado contiene el archivo elegido, `conflicts={}`, y un registro causal de que el tombstone conflictivo fue observado y resuelto. Si se elige borrar, se crea un nuevo tombstone `(3,2)` que domina la edición.

**Riesgo de recolectar tombstones**
Si B borra el tombstone demasiado pronto y luego recibe desde A una copia antigua del archivo `(1,1)`, puede no tener memoria causal del borrado `(1,2)`. Entonces podría tratar el archivo viejo como válido y resucitar datos borrados. El tombstone debe conservarse hasta que todos los nodos conocidos hayan reconocido un vector que domina el borrado y no puedan reintroducir estados anteriores.

**Ambigüedad corregida**
Antes dije `merged.deleted=false` en edición-vs-borrado. Eso es ambiguo: podría parecer que el archivo existe. Corrección: el estado debe ser `status=conflict`, sin contenido canónico, con variantes explícitas `{edit_variant, delete_variant}` y vector `(2,2)`.

All-cloud answer
#

Estado inicial: archivo vivo con vector `(1,1)`. Durante la desconexión:

- A edita: versión viva `EA = (2,1)`.
- B borra: tombstone `DB = (1,2)`.

Estos vectores son concurrentes: `(2,1)` no domina a `(1,2)` y `(1,2)` no domina a `(2,1)`.

**Estado final esperado**

Cuando A recibe el borrado `DB`, fusiona `EA` con `DB` y obtiene un conflicto explícito: “edición de A” frente a “borrado de B”. El archivo no debe quedar simplemente vivo ni simplemente borrado; debe quedar en estado conflictivo con ambas variantes preservadas.

Luego A recibe un duplicado del archivo inicial `(1,1)`. Como `(2,1)` y `(1,2)` dominan causalmente a `(1,1)`, ese duplicado se ignora.

Cuando B recibe la edición `EA`, fusiona `DB` con `EA` y debe obtener exactamente el mismo conflicto. Luego recibe un duplicado del borrado `DB`; como ya está incluido en el estado conflictivo, no cambia nada.

Resultado final en ambos: conflicto `{EA viva, DB tombstone}`, vector resumen `(2,2)` o conjunto causal equivalente, con contenido de A preservado y borrado de B preservado.

**Propiedades necesarias de la fusión**

La fusión debe ser:

- Conmutativa: `merge(EA, DB) = merge(DB, EA)`.
- Asociativa: el agrupamiento de mensajes no cambia el resultado.
- Idempotente: duplicados como `(1,1)` o `DB` no alteran el estado.
- Causal: una versión dominada por el estado actual no puede resucitar ni borrar nada.
- Conservadora ante concurrencia: si edición y borrado son concurrentes, no se descarta ninguno silenciosamente.

**Resolución explícita**

El usuario o una política superior puede resolver el conflicto creando una nueva operación causalmente posterior, por ejemplo en A:

`R = (3,2)` si A ya conoce `DB`, con decisión “mantener contenido editado” o “confirmar borrado”.

Si mantiene el archivo, `R` contiene el contenido elegido y domina tanto `EA` como `DB`. Si confirma borrado, `R` es una nueva tombstone que también domina ambas variantes. Al propagarse, `R` reemplaza el conflicto porque ya no hay concurrencia: hay una decisión posterior.

**Riesgo de recolectar tombstones pronto**

Si B borra la tombstone `DB` antes de que A la haya visto, A puede reenviar una versión viva antigua o concurrente y B no tendrá memoria causal suficiente para saber que hubo un borrado. El archivo puede “resucitar” como si el borrado nunca hubiera existido.

**Ambigüedad corregida**

Antes dije que un conflicto podía tener una variante “visible” elegida determinísticamente. Eso puede inducir a pérdida semántica. Mejor: el estado canónico debe ser un conjunto de variantes concurrentes, sin variante ganadora. La presentación puede destacar una, pero la sincronización debe conservar todas hasta una resolución causal posterior.

T5
#

My original prompt
#

[MXHYB-20260926-A T5] Una tarea pequeña para la documentación de Puente: traduce al inglés solo esta frase: «Puente conserva las ediciones concurrentes y pide una resolución explícita de los conflictos». Responde únicamente con la traducción.

Hybrid answer — local
#

🖥️ Puente preserves concurrent edits and requires explicit resolution of conflicts.

All-cloud answer
#

Puente preserves concurrent edits and requires explicit conflict resolution.

T6
#

My original prompt
#

[MXHYB-20260926-A T6] Revisión crítica final de Puente: un compañero propone guardar solo el vector máximo por componente y un único contenido elegido por hash; eliminar tombstones cuando ambos servidores hayan visto su vector; y, tras perder el disco, reutilizar el identificador del servidor con contador cero. Evalúa rigurosamente las tres decisiones, distingue convergencia de conservación de intención y explica un contraejemplo concreto para cada decisión insegura. Formula los supuestos mínimos bajo los que recolectar tombstones sí sería seguro, incluyendo mensajes antiguos en tránsito y réplicas restauradas. Cierra con tres pruebas de propiedades que detectarían estos fallos. Unas 450 palabras, sin ejecutar ni crear archivos.

Hybrid answer — cloud
#

☁️ **Hallazgos críticos**
- Guardar solo el vector máximo y un único contenido elegido por hash puede converger, pero no conserva intención. Si A edita “precio=10” con `(2,1)` y B edita “precio=12” con `(1,2)`, el máximo `(2,2)` no indica que hubo dos valores concurrentes; elegir por hash descarta una edición válida sin resolución explícita. Es convergente porque ambos acabarán con el mismo hash, pero semánticamente incorrecto: convierte un conflicto en una pérdida silenciosa.

- Eliminar tombstones cuando ambos servidores “han visto su vector” solo es seguro si esa frase significa reconocimiento causal estable y no simple recepción momentánea. Contraejemplo: B borra `f` con `(1,2)`; A lo ve y ambos eliminan el tombstone. Después llega un mensaje antiguo en tránsito con el archivo inicial `(1,1)`. Si ya no queda memoria del borrado, el archivo puede reaparecer. La convergencia local no basta: hace falta impedir la resurrección de estados causalmente obsoletos.

- Reutilizar el identificador del servidor con contador cero tras perder el disco rompe la unicidad de dots y la causalidad. Contraejemplo: antes del fallo, A generó `(A,5)` y B lo conserva. Tras reinstalarse como `A` desde cero, A genera otra operación `(A,1)`, `(A,2)`... B puede considerarlas dominadas por su vector `{A:5}` e ignorarlas, o confundirlas con historia antigua. Resultado: actualizaciones nuevas desaparecen o se mezclan con generaciones anteriores.

**Convergencia vs intención**
Convergencia significa que réplicas que reciben la misma información terminan en el mismo estado. Conservación de intención significa que operaciones concurrentes no se sobrescriben sin una relación causal o una resolución explícita. Un algoritmo “elige hash menor” puede ser determinista, conmutativo e idempotente, pero sigue siendo una política de pérdida silenciosa.

**Cuándo recolectar tombstones**
La recolección es segura solo bajo supuestos mínimos fuertes:
- membresía cerrada: se conocen todas las réplicas que pueden emitir o reenviar estados;
- identificadores de réplica persistentes y no reutilizados;
- cada réplica ha confirmado de forma duradera un vector que domina el tombstone;
- no existen mensajes antiguos en tránsito que puedan reaparecer sin ser filtrados por memoria causal;
- las restauraciones desde backup conservan, como mínimo, el vector causal reconocido y la información de borrados aún no estabilizados;
- si una réplica pierde esa memoria, debe reincorporarse con nuevo identificador/generación y sincronización completa, no reutilizando contadores antiguos.

Con solo A y B, puede recolectarse un tombstone de B cuando A sabe duraderamente que B sabe que A también ha observado un vector que domina el borrado, y ambos conservan una frontera causal estable que rechaza estados anteriores. Si no hay control de colas, snapshots, backups y mensajes duplicados, la recolección debe retrasarse o mantener un resumen causal compacto.

**Pruebas de propiedades**
- Concurrencia edición-edición: generar dos ediciones desconectadas y verificar que el estado final contiene ambas variantes o una fusión explícitamente marcada, nunca un único contenido arbitrario.
- Borrado contra mensaje antiguo: borrar, sincronizar, intentar reinyectar un estado anterior y comprobar que no resucita el archivo tras compactación.
- Pérdida de disco: reiniciar una réplica con el mismo ID y contador cero debe ser rechazado o detectado; con nuevo ID/generación, sus nuevas operaciones no deben ser confundidas con historia vieja.

All-cloud answer
#

La propuesta mezcla convergencia superficial con preservación semántica. Que todos acaben con el mismo estado no basta: Puente debe conservar intenciones concurrentes hasta una resolución causal explícita.

**1. Vector máximo + único contenido por hash**

Es inseguro. Guardar solo el máximo componente a componente, por ejemplo unir `(2,1)` y `(1,2)` en `(2,2)`, destruye la información de que eran versiones concurrentes. Si además se elige un único contenido por hash, se obtiene convergencia, pero con pérdida silenciosa.

Contraejemplo: A y B parten de `x=0` con `(1,1)`. Desconectados, A escribe `x=10` → `(2,1)` y B escribe `x=20` → `(1,2)`. Al sincronizar, ambos guardan vector `(2,2)` y el contenido con hash menor. Convergen, sí, pero la edición perdedora desaparece y el vector resultante finge que existe una versión que causó ambas decisiones, aunque nadie resolvió el conflicto.

Corrección: el estado debe contener un conjunto de variantes concurrentes o un registro de conflicto; el vector máximo puede ser resumen, no sustituto del historial causal relevante.

**2. Eliminar tombstones cuando ambos servidores hayan visto su vector**

Puede ser seguro solo bajo supuestos estrictos, pero la frase es incompleta. Si hay exactamente A y B, almacenamiento estable, identificidades persistentes, canales que no entregan mensajes anteriores tras el acuse, y ambos han confirmado haber integrado una versión posterior que domina la tombstone, entonces se puede recolectar.

Contraejemplo inseguro: B borra `f` con `(1,2)`. A aún tiene en una cola antigua una edición viva `(2,1)` enviada antes o durante la partición. Ambos intercambian acuses de la tombstone y B la recolecta. Después llega el mensaje viejo `(2,1)` a B o B restaura una copia anterior que no recordaba la tombstone. Si no queda memoria causal del borrado, el archivo puede resucitar o volver a conflicto falso.

Supuestos mínimos para GC: membresía cerrada o épocas explícitas; todos los miembros de la época han persistido un estado que domina la tombstone; no existen remitentes autorizados capaces de enviar estados no dominados; los canales o logs garantizan que mensajes antiguos anteriores a la barrera han sido drenados, descartados por época, o serán rechazados por un filtro causal persistido; las restauraciones desde backup deben traer un vector de réplica/época no ambiguo o requerir resincronización completa antes de emitir cambios. En la práctica, conservar un “floor” causal persistente por ruta o por réplica evita aceptar operaciones por debajo de lo ya recolectado.

**3. Reutilizar identificador con contador cero tras perder disco**

Es muy inseguro. Los identificadores de réplica y contadores forman la unicidad de operaciones. Reutilizar `A` con contador cero hace indistinguibles operaciones nuevas de antiguas o dominadas.

Contraejemplo: antes del fallo, A emitió `(5,3)` y B lo conserva. A pierde disco, vuelve como `A` contador `0`, edita y emite `(1,3)` o `(1,0)`. B puede tratarlo como antiguo y descartarlo, o mezclarlo causalmente con operaciones previas de A. Peor: si reaparecen mensajes antiguos de A, pueden colisionar con nuevos `op_id`. Resultado: pérdida, resurrección o divergencia.

Corrección: tras pérdida de disco, generar nueva identidad de réplica o recuperar contador/epoch desde almacenamiento duradero. Alternativamente, reincorporarse mediante protocolo de rejoin que asigna nueva época y prohíbe emitir operaciones hasta descargar un snapshot válido.

**Pruebas de propiedades**

- Concurrencia preservada: para dos ediciones concurrentes distintas, tras cualquier orden y duplicación de mensajes, ambas variantes siguen presentes hasta una resolución explícita.
- No resurrección tras borrado: una versión viva no dominante enviada antes de una tombstone recolectada no puede recrear el archivo.
- Identidad única: después de crash/rejoin, ninguna operación nueva puede compartir `op_id` ni ser ordenada falsamente bajo una época anterior.

Provenance
#

Extracted from the prompt and response fields of turn-1.json through turn-6.json in each run. All six prompts match the prompts.json fixture. I have neither rewritten nor corrected the answers. The article contains the usage table and comparison limitations.