happyin.work / блог

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

Несколько Claude в одном репо. Локи, handoff-ы и почта из 1982 года

Автор: Anastasiia Butova, инженер по ComfyUI и диффузионным моделям, Белград · опубликовано 2026-04-23 · обновлено 2026-09-23

Перевод английской версии: English

Я запускаю Claude Code параллельно на трёх подписках Pro, а мои коллеги ведут свои, часто в тех же файлах и в тот же день. В этом посте три задачи координации, которые из этого выросли, в порядке появления: передача контекста от одной сессии к другой, эксклюзивный доступ одной сессии к ресурсу и обмен сообщениями между двумя живыми сессиями. Каждая оказалась распределённо-системным паттерном возрастом около сорока лет: журнал только на добавление, lock-файл с heartbeat и почта. В конце: как эти три вещи упакованы в mclaude, что дало реальное использование и где это ломается.

Handoff-ы появились первыми, когда мне надоело пересказывать каждому новому чату, на чём мы остановились. Rollup-ы появились, когда в долгих проектах скопилось по восемьдесят handoff-ов. Почта появилась, когда я устала пересылать статусы Claude моих коллег через мессенджер.

Слой 1: handoff-ы, «я закончила, подхватывай»

Первая попытка: записывать всё в один HANDOFF.md при закрытии сессии. Ломается, как только чатов больше одного. Последний записавший побеждает, все остальные перезаписаны.

Поэтому я перешла на append-only. Каждая сессия пишет свой файл с уникальным именем:

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

В имени закодированы дата, время, первые восемь символов id сессии и slug задачи. Два чата не могут получить одно имя: даже в одну секунду их id сессий различаются. Гонка невозможна по конструкции, без блокировок и без брокера.

INDEX.md тоже только на добавление: новая строка в конце, никогда не правка выше. Чтобы перевести статус из ACTIVE в CLOSED, дописывается новая запись, а старая не редактируется. Так работает write-ahead log в базе данных, и по той же причине: если процесс умирает посреди записи, журнал остаётся согласованным.

Handoff — не дневник, а контракт со следующей сессией. Пять разделов, каждый отвечает на один вопрос: цель, что сделано (с путями, чтобы ничего не пересоздавать), что НЕ сработало и почему, текущее состояние (работает, сломано, заблокировано) и одно следующее действие. Одна строка на раздел из реального сеанса по цветовому чекеру:

  • Цель: «Colour checker: CNN sweep + diffusion, first results»
  • Сделано: «CNN baseline median 1.99 deg, 11M params, 21 MB»
  • Не сработало: «lr=3e-4, NaN after epoch 10-13, no gradient clipping»
  • Сломано: «Diffusion training OOM at bs=16»
  • Следующий шаг: «Inference script for diffusion + visual sheets»

Полный текст handoff-а из того же проекта — в Мой CLAUDE.md — 582 строки.

Раздел, который окупается, — «не сработало». За четыре дня накопилось 27 handoff-ов, и ни один тупик не повторился дважды, потому что каждая новая сессия видела предупреждение. Handoff умещается примерно в 1500 токенов против примерно 100 тысяч токенов сырого контекста сессии: примерно в 67 раз меньше, и при этом не теряется ничего, что важно для продолжения.

Запись автоматизирована. Stop-hook проверяет, как долго шла сессия, и если она была существенной и ничего не записано, спрашивает, сохранить ли handoff. Кроме того, в долгих сессиях сама модель предлагает записать его перед продолжением, обычно после крупной задачи или когда контекст заполнен на 70–80 %. Моих hook-ов проверки длины для этого нет; это решение самой модели. Чтение — одна фраза в начале следующей сессии: «подхвати handoff по цветовому чекеру» или ссылка на файл.

Rollup-ы, когда handoff-ов становится много

Если работать над чем-то месяц-два, накапливаются десятки handoff-ов, полные уже пройденных тупиков и уже опровергнутых гипотез. Новая сессия читает последние несколько, не копает глубже и спокойно предлагает подход, который я опровергла две недели назад.

Лекарство — свернуть старые. Один rollup суммирует двадцать предыдущих handoff-ов, отмечает дату границы и помечает каждый покрытый handoff строкой rolled_up_into: с именем файла rollup-а. Новая сессия читает один rollup и только handoff-ы, датированные после границы. Остальное остаётся для истории, но в контекст не попадает.

У rollup-а в имени файла есть сегмент rollup_, чтобы он выделялся в INDEX.md, а во frontmatter перечислено, что он покрывает (формат из README mclaude, сокращённый):

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

Тело — обычный handoff, который описывает двадцать сессий вместо одной. Паттерн не мой. PavelMuntyan/MF0-1984 решил ту же задачу схемой SQLite со столбцом covered_until_message_id, а я заменила таблицу на frontmatter в Markdown. Ничего хитрого: привычное «краткое содержание предыдущих глав».

Слой 2: локи, «этот ресурс сейчас мой»

Следующая проблема возникла с обучающими прогонами на удалённых серверах. Handoff-ы только дописываются, поэтому никто никого не перезаписывает. Настоящие ресурсы устроены иначе. Две сессии не могут одновременно рефакторить один модуль (конфликт слияния), не могут одновременно катить на staging (второй перезапишет первого), не могут одновременно обучаться на одной GPU (второй получит OOM). Одна из них должна удерживать ресурс эксклюзивно.

Один файл на ресурс, назван по нему: auth-middleware.lock, staging-deploy.lock, gpu-host-a_gpu2.lock. Внутри: кто взял, когда и зачем. Новая сессия смотрит в папку, видит, что занято, и выбирает свободное без вопросов ко мне.

Хитрость в том, как создаётся файл. os.open(path, O_CREAT | O_EXCL) атомарен: если файл уже есть, вызов падает. Два процесса не могут создать один файл одновременно, потому что это гарантирует ядро. Это тот же примитив, на котором с 1980-х держатся /var/lock и pid-файлы.

Лок без heartbeat переживает только до первого падения: сессия умерла, лок остался, и все ждут вечно. Поэтому по файловому соглашению активная сессия обновляет временную метку каждые 30–60 минут, а лок, чей heartbeat старше примерно четырёх часов, возможно мёртв. Возможно — не значит точно. Прежде чем перехватить, я проверяю сам ресурс: ps по SSH, nvidia-smi на GPU, очередь деплоя, git status модуля — всё, что скажет, действительно ли он занят или его просто никогда не освобождали. Только после этого беру. Chubby и ZooKeeper тоже проверяют снаружи перед перехватом, по той же причине.

Рядом с каждым локом лежат метаданные в JSON: slug задачи, id сессии, время взятия, описание, список файлов. Новая сессия читает .claude/locks/ и за секунду видит, что auth-middleware держит ani на рефакторинге, второй час, а GPU 2 занята обучением эмбеддингов. Вопрос «что тут происходит?» больше не возникает.

Моим первым порывом был PreToolUse-hook, который парсит команды и сам помечает ресурсы занятыми: команда трогает ресурс X — значит, X занят. Тупик. Команды слишком разнообразны, и парсер на регулярках превращается в машину ложных срабатываний. Правильный порядок был обратный: сначала соглашение и файлы, автоматизация потом и только для повторяющихся паттернов.

Слой 3: почта, «Артём, посмотри мой код»

Handoff-ы и rollup-ы переносят контекст во времени. Локи арбитражат ресурсы. Третья задача не подходит ни под один из этих слоёв: держать две живые сессии в синхронизации.

Типичный день: я поставила Claude коллеги переписать auth-middleware и ушла писать плагин в своём чате. Через час хотелось узнать, как там дела и не застрял ли он. Старый ответ: я открываю мессенджер, пишу коллеге, коллега смотрит в свой чат и копирует текст обратно. Три пересылки через человека каждый вечер, по нескольким проектам.

Мне нужны были адресные сообщения между сессиями, которые могут никогда не быть онлайн одновременно, с гарантией доставки: если сообщение не прочитано сейчас, оно будет прочитано в следующей сессии. Я подумала и поняла, что описываю почту.

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

Каждое сообщение — отдельный Markdown-файл с YAML frontmatter (from, to, type, subject, message_id, in_reply_to, status) и письмом под ним. Простой случай: сессия ani отправляет Артёму вопрос с темой «Re: cmake libsodium» и текстом «Ты менял линковку libsodium в CMakeLists? Сборка падает на target_link_libraries». Через час Артём открывает Claude Code на своей машине, UserPromptSubmit-hook проверяет его inbox до первого промпта, видит непрочитанное и кладёт его в контекст. Он отвечает на месте, и ответ падает в мой inbox.

Ветки идут через in_reply_to, как в RFC 822. Подтверждения доставки — отдельные файлы. Сообщение на to: * попадает в all/, которую проверяют все. Для дедупликации hook запоминает в .watcher_state.json, какие сообщения уже показал, так что ничто не попадает в контекст дважды. Ни опроса, ни брокера, ни фонового демона: всё держится на hook-е, который срабатывает в нужный момент. Каждая функция соответствует классической почте один к одному:

Почта (RFC 822, 1982) Почтовый ящик агентов
To / Cc адресация конкретной сессии
From / Reply-To обратный адрес для ответа
Subject сортировка без чтения тела
In-Reply-To ветки сообщений
Date насколько свежий запрос
Read / Unread видели или ещё нет
Inbox на пользователя очередь на получателя
Sent folder журнал аудита отправителя
Delivery receipt подтверждение без «ты видел?»

mclaude: три слоя как один инструмент

Я упаковала слои в открытый инструмент mclaude: Python, без зависимостей в ядре, 193 теста, версия 0.6.0. Как я описывала его тогда, у него было шесть слоёв: три базовых и три добавленных позже:

Слой Что делает Строк кода
Locks атомарный захват ресурса + heartbeat 469
Handoffs сводки по сессиям, append-only INDEX 371
Memory Graph иерархическое хранилище решений и подводных камней ~400
Identity Registry имена для сессий (ani, artem, nastya) 246
Messages + Active Mail обмен сообщениями между сессиями, с ветками 400 + 236
Code Indexer AST-сканер → code-map.md + llms.txt новое в v0.6.0

Индексатор обходит проект и пишет два файла для агента: code-map.md, карту модулей, классов и функций, и llms.txt, компактный индекс для быстрой загрузки. Новая сессия не тратит первые минуты на перечитывание кода, потому что карта уже лежит в репозитории рядом с handoff-ами.

Поверх этого идут пять hook-ов: четыре на события Claude Code (SessionStart, PreToolUse, UserPromptSubmit, Stop) и git pre-commit guard, который отказывает в коммите, затрагивающем файл, захваченный чужим локом. MCP-сервер с более чем 20 инструментами отдаёт структурированный JSON прямо в Claude Code, без парсинга вывода CLI.

Вот как выглядит лок из командной строки, из README 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

Правило дизайна: хаб — ускоритель, а не зависимость. Всё работает на файлах. Когда хаб поднят, сообщения летят по WebSocket в реальном времени. Когда хаб упал, мост пишет в тот же .claude/messages/inbox/ в том же формате, и получатель после git pull видит ровно то же самое. Удалите пакет mclaude — файлы в .claude/ остаются читаемыми: Markdown для рассказа, JSON для метаданных.

mclaude demo --no-pause занимает около 30 секунд. Он запускает две смоделированные сессии, ani и vasya, через слои и печатает диаграмму Mermaid того, что произошло; она рендерится как есть в pull request или в посте.

Пробел в экосистеме

Потом я пошла смотреть, кто ещё решает эту задачу. Изоляция имеет много ответов: git worktrees (parallel-cc), песочницы (Kmux), отдельный порт на каждого агента с проброшенными переменными окружения, Agent Teams от Anthropic с очередью задач и захватами. Все они решают «сессии не мешают друг другу», делая так, чтобы общего состояния не было.

Координация общего состояния почти пуста. Только claude_code_agent_farm Dicklesworthstone делает файловые локи, но это оркестрация для 20+ агентов в tmux, а не много чатов над одним проектом. Трекер самого Claude Code показывает пробел с другой стороны: #19364, запрос на lock-файл сессии, так и не был реализован, а #29217 документирует, как .claude.json портится от одновременных записей из нескольких сессий.

Экосистема зрелая в том, чтобы не трогать общее состояние, и почти пуста в том, чтобы его координировать.

Ничего из этого не ново. Таймауты heartbeat, lock-файлы, названные по ресурсу, журнал, в который только дописывают, внешняя проверка перед перехватом протухшего лока, один файл на ресурс вместо одной общей таблицы: Chubby от Google, ZooKeeper и любая система координации последних сорока лет стоят на этих же вещах. Агенты ИИ — ещё один вид процесса в распределённой системе, которую мы уже понимаем.

RFC 822 подходит по той же причине. Почта решает ровно эту абстрактную задачу: асинхронная доставка между процессами, которые могут быть неактивны одновременно, с ветками и подтверждением доставки. Каждое поколение распределённых систем заново открывает, что почта была права. В 2010-х это были очереди сообщений, RabbitMQ и Kafka. В 2026-м — AI-агенты.

Реальное использование

Три человека, один проект C++-плагина, шесть параллельных сессий Claude Code, каждый на своей части: архитектура, конвейер инференса, интерфейс.

Один агент решил поменять вывод ключа в модуле scramble и разослал широковещательное сообщение через mailbox/all/. Другой агент увидел его в начале своей следующей сессии, до того как написать хоть строчку кода, и учёл изменение. Без почты он написал бы код под несовместимый формат: в лучшем случае конфликт слияния, в худшем — испорченные данные.

Передача handoff-ов между подписками, когда работа заканчивалась в одной и продолжалась в другой, сэкономила около четырёх часов, которые иначе ушли бы на восстановление контекста.

Где это ломается и чего я не знаю

  • Масштаб. Проверено на 3–6 параллельных сессиях, 2–3 людях, одном C++-проекте, плюс много сессий на сайте. Я не измеряла 20+ сессий от 10 агентов. Подозреваю, что первым ломается inbox: дедупликация через .watcher_state.json линейна по числу сообщений и замедлится на сотнях.
  • Несколько машин без хаба. Файловое ядро синхронизируется через общий репозиторий, так что другая сторона увидит сообщение после git pull: минуты, а не секунды. Хаб даёт секунды по WebSocket, но это ещё один процесс, который надо держать запущенным, и в продакшене я его не пробовала.
  • Гонки локов на сетевых файловых системах. O_CREAT | O_EXCL атомарен на локальной файловой системе. На NFS или SMB гарантия зависит от реализации, поэтому тому, кто положит .claude/locks/ туда, может столкнуться с гонкой.
  • Перехват протухших локов полуавтоматический. Проверки через nvidia-smi, ps и git status написаны под человека. Их автоматизация требует обработчика, который знает, как проверять каждый тип ресурса.
  • Почта не будит сессию. Только с hook-ом сообщения появляются при UserPromptSubmit, то есть только когда активная сессия что-то печатает. Настоящей push-доставки нет (см. обновление ниже).
  • Человек всё ещё в цикле. Если шесть агентов решат одновременно, что auth-middleware надо переписать, они договорятся почтой, но кто возьмёт работу, — решение человека. mclaude даёт инструменты для переговоров, а не автономную оркестрацию.

Что изменилось после апреля

Обновление, сентябрь 2026. Три поправки и два добавления, все проверены 23 сентября 2026 года.

  • Установка. pip install mclaude, как я писала в апреле, не работает: пакет не опубликован на PyPI, там возвращается 404. Ставьте из клона репозитория, как ниже.
  • Issue #19364. Я описывала его как открытый запрос. Бот, который закрывает устаревшие issue, уже закрыл его как not planned 28 февраля 2026 года, до выхода этого поста. Его так и не реализовали, и это тот самый пробел, о котором говорит раздел, но он не был открыт.
  • Issue #29217. Я приводила цитату «315 broken backups in seven days». Этой цифры в ветке в том виде, в каком она есть сейчас, нет. Исходный отчёт считает 165 повреждённых бэкапов примерно за 40 минут, а один комментатор насчитал не менее 423 повреждений за семь дней. Issue закрыли как неактивное 25 мая 2026 года.
  • Седьмой слой. README mclaude также описывает heartbeat сессий на файлах, о котором этот пост не рассказывает, и пакет держит локи строже, чем соглашение выше: heartbeat каждые 30 секунд, а лок считается протухшим после 3 минут молчания.
  • Почта в середине разговора. Первый план из конца русскоязычного оригинала был реализован в тот же день. Pull request #5, слитый в 17:22 UTC 23 апреля, через несколько часов после публикации поста, добавляет скрипт для инструмента Monitor в Claude Code: он опрашивает inbox каждые 30 секунд и передаёт каждое новое сообщение работающей сессии без того, чтобы кто-то печатал промпт. Значит, ограничение «почта не будит сессию» верно только для настройки, где есть лишь hook.

Основная ветка без коммитов с момента того слияния. Репозиторий MF0-1984, откуда взята идея rollup-ов, теперь возвращает 404.

Попробовать

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

Лицензия MIT, Python 3.9+. Принципы описаны отдельно как принцип 18, координация нескольких сессий, в репозитории конфига, описанном на странице codex-claude-code-config. Страница проекта инструмента — mclaude. Почему я полагаюсь на handoff-ы в собственном репозитории, а не на историю сессий desktop-приложения, объяснено в 45 МБ сессий Claude Code, которые вы никогда не открываете.

Русский оригинал опубликован на Хабре.

Другие статьи

Ещё на сайте

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