happyin.work / blog

English · Русский · Español · Deutsch · Português (Brasil) · 简体中文 · 日本語

Varios Claude en un mismo repo. Bloqueos, handoffs y correo de 1982

Autora: Anastasiia Butova, ingeniera de ComfyUI y modelos de difusión, Belgrado · publicado 2026-04-23 · actualizado 2026-09-23

Traducción de la versión en inglés: English

Uso Claude Code en paralelo con tres suscripciones Pro, y mis colegas usan las suyas, a menudo sobre los mismos archivos el mismo día. Este post cubre los tres problemas de coordinación que surgieron de eso, en el orden en que aparecieron: pasar contexto de una sesión a la siguiente, dar a una sesión el uso exclusivo de un recurso y dejar que dos sesiones vivas se hablen entre sí. Cada uno resultó ser un patrón de sistemas distribuidos de unos cuarenta años: un registro de solo añadir, un archivo de bloqueo con latido y el correo electrónico. Al final: cómo se empaquetan los tres como mclaude, cómo fue el uso real y dónde se rompe.

Los handoffs llegaron primero, cuando me cansé de volver a contar el estado de las cosas a cada chat nuevo. Los resúmenes llegaron cuando los proyectos largos acumularon ochenta handoffs cada uno. El correo llegó cuando me cansé de retransmitir por un mensajero los estados de Claude de mis colegas.

Capa 1: handoffs, "he terminado, retómalo"

El primer intento: escribir todo en un único HANDOFF.md al cerrar la sesión. Se rompe en cuanto hay más de un chat. Gana el último que escribe, y los demás quedan sobrescritos.

Así que pasé a solo añadir. Cada sesión escribe su propio archivo con un nombre único:

.claude/handoffs/
├── 2026-04-09_14-32_373d1618_drift-validator.md
├── 2026-04-09_16-47_b858f500_dashboard-refactor.md
├── 2026-04-10_11-20_a1c2d3e4_auth-fix.md
└── INDEX.md   (append-only log)

El nombre codifica la fecha, la hora, los ocho primeros caracteres del id de sesión y un slug de la tarea. Dos chats no pueden producir el mismo nombre: incluso en el mismo segundo, sus ids de sesión son distintos. La carrera es imposible por construcción, sin bloqueos ni intermediario.

INDEX.md también es de solo añadir: una línea nueva al final, nunca una edición encima. Para pasar un estado de ACTIVE a CLOSED, añades un registro nuevo en lugar de editar el antiguo. Un write-ahead log de una base de datos funciona exactamente así, y por la misma razón: si un proceso muere a mitad de escritura, el registro sigue siendo coherente.

Un handoff no es un diario, es un contrato con la siguiente sesión. Cinco secciones, una pregunta cada una: el objetivo, lo que se hizo (con rutas, para que nada se reconstruya), lo que NO funcionó y por qué, el estado actual (funcionando, roto, bloqueado) y una acción siguiente. Una línea por sección de una sesión real de un comprobador de color:

  • Objetivo: "Colour checker: CNN sweep + diffusion, first results"
  • Hecho: "CNN baseline median 1.99 deg, 11M params, 21 MB"
  • No funcionó: "lr=3e-4, NaN after epoch 10-13, no gradient clipping"
  • Roto: "Diffusion training OOM at bs=16"
  • Siguiente paso: "Inference script for diffusion + visual sheets"

El texto completo de un handoff del mismo proyecto está en Mi CLAUDE.md tiene 582 líneas.

La sección que se gana su sitio es "no funcionó". En cuatro días se acumularon 27 handoffs y ningún callejón sin salida se recorrió dos veces, porque cada sesión nueva veía la advertencia. Un handoff cabe en unos 1.500 tokens frente a unos 100K de contexto bruto de sesión: unas 67 veces menos, sin perder nada que importe para seguir.

La escritura está automatizada. Un hook de Stop comprueba cuánto duró la sesión y, si fue sustancial y no se registró nada, pregunta si guardar un handoff. Además, en sesiones largas el propio modelo ofrece escribir uno antes de seguir, normalmente tras una tarea grande o cuando el contexto está entre un 70 y un 80 % lleno. Mis hooks no tienen ninguna comprobación de longitud para eso; es decisión del modelo. La lectura es una frase al empezar la siguiente sesión, "retoma el handoff del comprobador de color", o un enlace al archivo.

Resúmenes, cuando los handoffs se acumulan

Si trabajas en algo durante un mes o dos, se acumulan decenas de handoffs, llenos de callejones sin salida ya recorridos e hipótesis ya descartadas. Una sesión nueva lee los últimos, no mira más allá, y tranquilamente propone un enfoque que descarté dos semanas antes.

La solución es plegar los antiguos. Un resumen condensa veinte handoffs anteriores, marca la fecha de frontera y sella cada handoff que cubre con una línea rolled_up_into: que nombra el archivo del resumen. Una sesión nueva lee un resumen más solo los handoffs posteriores a su frontera. Los demás se quedan como historial, pero no entran en el contexto.

El resumen lleva un segmento rollup_ en el nombre de archivo para destacar en INDEX.md, y su frontmatter enumera lo que cubre (el formato del README de mclaude, acortado):

---
type: rollup
covers:
  - 2026-03-01_12-00_session-01_kickoff
  - 2026-03-02_14-30_session-02_auth-refactor
  - 2026-03-15_18-00_session-12_ci-pipeline
through: 2026-03-15 18:00
---

El cuerpo es un handoff normal que describe veinte sesiones en lugar de una. El patrón no es mío. PavelMuntyan/MF0-1984 resolvió el mismo problema con un esquema SQLite y una columna covered_until_message_id, y yo cambié la tabla por frontmatter de Markdown. Nada ingenioso: el conocido "resumen de capítulos anteriores".

Capa 2: bloqueos, "este recurso es mío ahora mismo"

El siguiente problema apareció con las ejecuciones de entrenamiento en servidores remotos. Los handoffs solo añaden, así que nadie sobrescribe a nadie. Los recursos reales son otra cosa. Dos sesiones no pueden refactorizar a la vez un mismo módulo (conflicto de fusión), no pueden desplegar a staging a la vez (la segunda sobrescribe a la primera), no pueden entrenar en una misma GPU a la vez (la segunda da un OOM). Una de ellas tiene que tener el recurso en exclusiva.

Un archivo por recurso, nombrado según el recurso: auth-middleware.lock, staging-deploy.lock, gpu-host-a_gpu2.lock. Dentro: quién lo tomó, cuándo y para qué. Una sesión nueva lista la carpeta, ve qué está tomado y elige algo libre sin preguntarme.

El truco está en cómo se crea el archivo. os.open(path, O_CREAT | O_EXCL) es atómico: si el archivo ya existe, la llamada falla. Dos procesos no pueden crear el mismo archivo a la vez, porque el kernel lo garantiza. Es la misma primitiva que hay detrás de /var/lock y de los archivos pid desde los años ochenta.

Un bloqueo sin latido solo sobrevive hasta el primer fallo: la sesión muere, el bloqueo queda, y todos esperan para siempre. Así que, en la convención de archivos, una sesión activa renueva su marca de tiempo cada 30 o 60 minutos, y un bloqueo cuyo latido tiene más de unas cuatro horas posiblemente está muerto. Posiblemente no es seguro. Antes de tomarlo, compruebo el propio recurso: ps por SSH, nvidia-smi en la GPU, la cola de despliegues, git status del módulo, lo que me diga si de verdad está ocupado o simplemente nunca se liberó. Solo entonces lo tomo. Chubby y ZooKeeper también verifican desde fuera antes de una toma de control, por la misma razón.

Junto a cada bloqueo hay metadatos en JSON: slug de la tarea, id de sesión, hora de toma, descripción, lista de archivos. Una sesión nueva lee .claude/locks/ y ve en un segundo que auth-middleware lo tiene ani para un refactor, segunda hora, y que la GPU 2 está ocupada entrenando embeddings. La pregunta "¿qué pasa aquí?" ya no surge.

Mi primer impulso fue un hook PreToolUse que analizara los comandos y marcara los recursos como ocupados automáticamente: un comando toca el recurso X, así que X está tomado. Callejón sin salida. Los comandos son demasiado variados, y un analizador con expresiones regulares se convierte en una máquina de falsos positivos. El orden correcto era el contrario: primero la convención y los archivos, después la automatización, y solo para los patrones que de verdad se repiten.

Capa 3: correo, "Artem, mira mi código"

Los handoffs y los resúmenes transportan contexto a través del tiempo. Los bloqueos arbitran recursos. Un tercer trabajo no encaja en ninguno: mantener sincronizadas dos sesiones vivas.

Un día típico: le encargué a Claude de un colega que refactorizara auth-middleware y me fui a escribir un plugin en mi propio chat. Una hora después quise saber cómo iba y si se había atascado. La respuesta de siempre: abro un mensajero y le escribo a mi colega, el colega mira su chat y me copia el texto de vuelta. Tres relevos por una persona, cada tarde, en varios proyectos.

Lo que necesitaba eran mensajes dirigidos entre sesiones que quizá nunca estén en línea a la vez, con entrega garantizada: si un mensaje no se lee ahora, se lee en la siguiente sesión. Lo pensé y me di cuenta de que estaba describiendo el correo electrónico.

.claude/mailbox/
├── ani/
│   ├── inbox/
│   ├── sent/
│   └── archive/
├── artem/
│   └── ...
└── all/          (broadcast)

Cada mensaje es un archivo Markdown separado con frontmatter YAML (from, to, type, subject, message_id, in_reply_to, status) y la carta debajo. Un caso sencillo: la sesión de ani le envía a Artem una pregunta con el asunto "Re: cmake libsodium" y el cuerpo "¿Cambiaste el enlazado de libsodium en CMakeLists? Mi build falla en target_link_libraries". Una hora después, Artem abre Claude Code en su máquina, un hook UserPromptSubmit comprueba su bandeja antes del primer prompt, ve el mensaje sin leer y lo pone en el contexto. Él responde en el acto, y la respuesta llega a mi bandeja.

Los hilos se forman con in_reply_to, como en RFC 822. Los acuses de recibo son archivos aparte. Un mensaje dirigido a to: * va a all/, que todos comprueban. Para evitar duplicados, el hook recuerda en .watcher_state.json qué mensajes ya mostró, así que nada entra dos veces en el contexto. Sin sondeo, sin intermediario, sin demonio en segundo plano: todo descansa en un hook que se dispara en el momento oportuno. Cada función corresponde uno a uno con el correo clásico:

Correo (RFC 822, 1982) Buzón de agentes
Para / Cc dirigirse a una sesión concreta
De / Responder a dirección de retorno para la respuesta
Asunto clasificar sin leer el cuerpo
In-Reply-To hilos de mensajes
Fecha lo fresca que es la petición
Leído / No leído visto o todavía no
Bandeja por usuario cola por destinatario
Carpeta de enviados registro de auditoría del remitente
Acuse de recibo confirmación sin "¿lo viste?"

mclaude: las tres capas como una sola herramienta

Empaqueté las capas como una herramienta de código abierto, mclaude: Python, sin dependencias en el núcleo, 193 pruebas, versión 0.6.0. Como lo describí entonces, tenía seis capas: tres básicas y tres añadidas después:

Capa Qué hace Líneas de código
Bloqueos toma atómica de recursos + latido 469
Handoffs resúmenes por sesión, INDEX de solo añadir 371
Memory Graph almacén jerárquico de decisiones y trampas ~400
Identity Registry nombres para sesiones (ani, artem, nastya) 246
Mensajes + Active Mail mensajería entre sesiones, con hilos 400 + 236
Code Indexer escáner AST → code-map.md + llms.txt nuevo en v0.6.0

El indexador recorre el proyecto y escribe dos archivos para el agente: code-map.md, un mapa de módulos, clases y funciones, y llms.txt, un índice compacto para cargar rápido. Una sesión nueva no gasta sus primeros minutos releyendo código, porque el mapa ya está en el repositorio junto a los handoffs.

Encima hay cinco hooks: cuatro en eventos de Claude Code (SessionStart, PreToolUse, UserPromptSubmit, Stop) y un guardián de pre-commit de git que rechaza un commit que toque un archivo bloqueado por otra persona. Un servidor MCP con más de 20 herramientas devuelve JSON estructurado directamente a Claude Code, sin analizar la salida de la CLI.

Así se ve un bloqueo desde la línea de comandos, según el README de mclaude:

$ mclaude lock claim --slug fix-auth-middleware-race \
    --description "Race condition when two requests write the same session key" \
    --files src/auth/middleware.py src/auth/session.py
[lock] claimed fix-auth-middleware-race

# a second session tries the same slug
$ mclaude lock claim --slug fix-auth-middleware-race --description "same bug I guess"
[lock] fix-auth-middleware-race already held
  by session: 665a44027f424db7
  doing:      Race condition when two requests write the same session key

La regla de diseño: el hub es un acelerador, no una dependencia. Todo funciona con archivos. Con el centro activo, los mensajes viajan por WebSocket en tiempo real. Con el centro caído, el puente escribe en la misma .claude/messages/inbox/ con el mismo formato, y el destinatario ve exactamente lo mismo tras un git pull. Si quitas el paquete mclaude, los archivos de .claude/ siguen siendo legibles: Markdown para la narrativa, JSON para los metadatos.

mclaude demo --no-pause tarda unos 30 segundos. Ejecuta dos sesiones simuladas, ani y vasya, a través de las capas e imprime un diagrama Mermaid de lo ocurrido, que se renderiza tal cual en un pull request o en un post.

La laguna del ecosistema

Luego fui a ver quién más estaba resolviendo esto. El aislamiento tiene muchas respuestas: worktrees de git (parallel-cc), sandboxes (Kmux), un puerto separado por agente con variables de entorno reenviadas, y Agent Teams de Anthropic con una cola de tareas y reclamaciones. Todos resuelven "las sesiones no se molestan entre sí" asegurándose de que no haya estado compartido.

La coordinación del estado compartido estaba casi vacía. Solo claude_code_agent_farm, de Dicklesworthstone, usa bloqueos de archivo, y es orquestación para más de 20 agentes en tmux, no muchos chats sobre un proyecto. El propio tracker de Claude Code muestra la laguna desde el otro lado: el #19364, una petición de un archivo de bloqueo de sesión, nunca se implementó, y el #29217 documenta un .claude.json corrompido por escrituras concurrentes de varias sesiones.

El ecosistema está maduro para evitar el estado compartido y casi vacío para coordinarlo.

Nada de esto es nuevo. Los timeouts de latido, los archivos de bloqueo con el nombre del recurso, un registro al que solo se añade, una comprobación externa antes de tomar un bloqueo caducado, un archivo por recurso en lugar de una tabla compartida: Chubby de Google, ZooKeeper y todo sistema de coordinación de los últimos cuarenta años descansan sobre esas mismas cosas. Los agentes de IA son solo un tipo más de proceso en un sistema distribuido que ya entendemos.

RFC 822 encaja por la misma razón. El correo resuelve exactamente este problema abstracto: entrega asíncrona entre procesos que quizá no estén activos a la vez, con hilos y confirmación de entrega. Cada generación de sistemas distribuidos vuelve a descubrir que el correo tenía razón. En los años diez fueron las colas de mensajes, RabbitMQ y Kafka. En 2026 son los agentes de IA.

Uso real

Tres personas, un proyecto de plugin en C++, seis sesiones paralelas de Claude Code, cada persona en su parte: arquitectura, el pipeline de inferencia, la interfaz.

Un agente decidió cambiar la derivación de claves en el módulo de scramble y envió un anuncio general a través de mailbox/all/. Otro agente lo vio al empezar su siguiente sesión, antes de escribir ningún código, y lo tuvo en cuenta. Sin el buzón habría escrito código contra un formato incompatible: en el mejor caso un conflicto de fusión, en el peor datos corruptos.

Los handoffs entre suscripciones, terminando en una y continuando en otra, ahorraron unas cuatro horas que, si no, se habrían ido en reconstruir el contexto.

Dónde se rompe, y lo que no sé

  • Escala. Probado con 3 a 6 sesiones paralelas, 2 o 3 personas, un proyecto de C++, además de muchas sesiones en un sitio web. No he medido 20 o más sesiones de 10 agentes. Sospecho que la bandeja se rompe primero: la deduplicación mediante .watcher_state.json es lineal en el número de mensajes y se volverá lenta con cientos.
  • Varias máquinas sin el centro. El núcleo de archivos se sincroniza por un repositorio compartido, así que el otro lado ve un mensaje tras un git pull: minutos, no segundos. El centro da segundos por WebSocket, pero es un proceso más que ejecutar, y no lo he probado en producción.
  • Carreras de bloqueo en sistemas de archivos de red. O_CREAT | O_EXCL es atómico en un sistema de archivos local. En NFS o SMB la garantía depende de la implementación, así que quien ponga .claude/locks/ ahí puede encontrarse una carrera.
  • La toma de un bloqueo caducado es semiautomática. Las comprobaciones con nvidia-smi, ps y git status están escritas para una persona. Automatizarlas exige un manejador que sepa comprobar cada tipo de recurso.
  • El correo no despierta una sesión. Solo con el hook, los mensajes aparecen en UserPromptSubmit, y únicamente cuando la sesión activa escribe algo. La entrega push real no está (ver la actualización de abajo).
  • Una persona sigue en el bucle. Si seis agentes deciden a la vez que auth-middleware hay que reescribirlo, negociarán por correo, pero quién lo toma sigue siendo una decisión humana. mclaude da las herramientas para negociar, no orquestación autónoma.

Qué cambió después de abril

Actualización, septiembre de 2026. Tres correcciones y dos añadidos, todos comprobados el 23 de septiembre de 2026.

  • Instalación. pip install mclaude, como lo escribí en abril, no funciona: el paquete no está publicado en PyPI, que devuelve 404. Instala desde un clon del repositorio, como más abajo.
  • Issue #19364. Lo describí como una petición abierta. Un bot de limpieza ya lo había cerrado como not planned el 28 de febrero de 2026, antes de que saliera este post. Nunca se implementó, que es el punto de la sección, pero no estaba abierto.
  • Issue #29217. Cité "315 copias de seguridad rotas en siete días". Esa cifra no aparece en el hilo tal como está ahora. El reporte original cuenta 165 copias corrompidas en unos 40 minutos, y un comentarista contó al menos 423 corrupciones en siete días. El issue se cerró por inactividad el 25 de mayo de 2026.
  • Séptima capa. El README de mclaude describe también latidos de sesión basados en archivos, que este post no cubre, y el paquete aplica bloqueos más estrictos que la convención manual de arriba: un latido cada 30 segundos, y un bloqueo cuenta como caducado tras 3 minutos de silencio.
  • Correo durante la conversación. El primer plan del final del original en ruso se publicó el mismo día. El pull request #5, fusionado a las 17:22 UTC del 23 de abril, unas horas después de publicar el post, añade un script para la herramienta Monitor de Claude Code: consulta la bandeja cada 30 segundos y entrega cada mensaje nuevo a la sesión en marcha sin que nadie escriba un prompt. Así que el límite "el correo no despierta una sesión" de arriba solo se cumple en la configuración con hook exclusivamente.

La rama principal no ha tenido commits desde esa fusión. El repositorio MF0-1984, de donde salió la idea de los resúmenes, ahora devuelve 404.

Probarlo

git clone https://github.com/AnastasiyaW/mclaude.git
cd mclaude
pip install -e .
mclaude hooks install --apply
mclaude demo --no-pause

Licencia MIT, Python 3.9+. Los principios detrás están descritos aparte como principio 18, coordinación entre varias sesiones, en el repositorio de configuración descrito en la página codex-claude-code-config. La página del proyecto de la herramienta es mclaude. Por qué confío en los handoffs de mi propio repositorio en lugar del historial de sesiones de la app de escritorio está en 45 MB de sesiones de Claude Code que nunca miras.

El original en ruso se publicó en Habr.

Más artículos

En otras partes del sitio

Anastasiia Butova - ML engineer, Belgrade, Serbia. Email [email protected] · LinkedIn · GitHub · Telegram · Hugging Face