Skip to content

Latest commit

 

History

History
367 lines (282 loc) · 24.8 KB

File metadata and controls

367 lines (282 loc) · 24.8 KB

codexSync

Утилита с открытым исходным кодом для переноса локального состояния Codex между персональными машинами через облачно-синхронизируемую папку.

Important

Сценарий проверен на практике только Windows → Windows. macOS поддержан в коде и в CI, но end-to-end handoff на реальных macOS-машинах ещё не валидирован.

Английская версия: README.md.

Зачем

Разработчику нужно продолжить работу с Codex на другой машине, не потеряв локальное состояние: проекты, привязки чатов и историю сессий.

Что делает

  • Синхронизирует локальный каталог состояния Codex через любую облачную папку (Dropbox, OneDrive, Яндекс.Диск, Syncthing…) — сначала резервная копия, и только при закрытом Codex.
  • Защищает глобальное состояние. guardian делает неизменяемые проверенные снимки .codex-global-state.json при работающем Codex, записывая их только вне .codex. После BSOD или аварийного завершения остаётся пригодный к восстановлению latest-good.
  • Восстанавливается после прерванной мутации. Каждая запись обёрнута в блокировку, долговечный журнал и проверенный backup, а recover inspect|resume|rollback — единственный санкционированный выход из операции, остановившейся на полпути.
  • Чинит переезд между машинами. repair-projects восстанавливает привязки проектов после смены путей, опираясь на то, что говорят сами сессии, и оформляет это неизменяемым планом, который вы подтверждаете его id.
  • Переносит историю сессий. sessions классифицирует каждую ветку семантически — идентична, fast-forward, архивный переход, расхождение — и никогда не сливает, не сортирует и не выбирает победителя по времени.
  • Находит чат и кладёт его под проект. chats показывает, что есть, и главное — почему каждый чат находится там, где находится.

Чего НЕ делает

  • Не интегрируется с внутренними механизмами Codex
  • Не использует API
  • Не извлекает токены
  • Не перехватывает сетевой трафик
  • Не синхронизирует в реальном времени
  • Не запускает и не завершает Codex
  • Не пишет в SQLite Codex — только читает
  • Не проверяет состояние облачного клиента и свободное место

Принципы дизайна

  • Сначала backup, при сомнении — отказ. Неопределённость никогда не трактуется оптимистично.
  • Одна инстанция власти, один конверт. Ровно одно место решает, можно ли менять состояние, и ровно один путь это изменение выполняет.
  • Говорить почему, а не угадывать. Ветка, которую нельзя классифицировать, проект с двумя кандидатами, неподтверждённое поведение рантайма — каждое сообщается кодом, а не аппроксимируется.
  • Ноль внешних зависимостей в ядре и CLI.
  • Windows в первую очередь; macOS поддержан в коде и CI.

Как это работает

  1. Определяется, запущен ли Codex. Ответ «не удалось определить» считается «запущен»: ничего мутирующего не делается оптимистично.
  2. Команды только на чтение (doctor, plan, любые scan, chats, guardian) работают в любом случае. Результат, полученный при открытом Codex, помечается volatile и не может быть переиспользован мутацией.
  3. Мутация выполняется только при закрытом Codex и всегда одним конвертом: неперехватываемая блокировка → долговечный журнал → проверенный backup всего, что будет заменено → финальная проверка процесса непосредственно перед коммитом → staging на том же томе → атомарная замена → COMMITTED.
  4. Любая неоднозначность останавливает операцию, а не разрешается догадкой, и код выхода говорит, какого рода была остановка.

Команды CLI

Запуск из корня проекта:

python -m codexsync -c config.toml <команда>

«Холодная» означает, что команда откажется работать при открытом Codex. Остальные только читают и работают в любой момент.

Команда Холодная? Что делает
init-config Создать config.toml из встроенного шаблона
validate нет Загрузить и проверить конфигурацию
doctor / preflight нет Диагностика окружения; команды идентичны и не имеют побочных эффектов
plan нет Показать, что скопировал бы sync
sync да Синхронизация в обе стороны, сначала backup
restore да Восстановление из проверенного снимка backup
guardian watch нет Непрерывно снимать глобальное состояние при работающем Codex
guardian snapshot --once нет Сделать один снимок сейчас
guardian scheduler нет Сгенерировать шаблоны пользовательского планировщика
repair-projects scan нет Построить неизменяемый план ремонта привязок
repair-projects apply да Применить ровно один план по его id
sessions scan нет Классифицировать все ветки сессий с обеих сторон
sessions index нет Показать содержимое session_index.jsonl с обеих сторон
sessions resolve нет Зафиксировать одно решение по расхождению
sessions apply да Перенести ветки целиком по подтверждённому плану
chats list / chats tree нет Найти чаты и увидеть, под каким они проектом
chats move да Перенести выбранные чаты под один проект
recover inspect нет Прочитать журнал мутации без побочных эффектов
recover resume / rollback да Закрыть прерванную мутацию

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

Начало работы

python -m codexsync init-config
python -m codexsync -c config.toml validate
python -m codexsync -c config.toml doctor
python -m codexsync -c config.toml plan
python -m codexsync -c config.toml sync --dry-run
python -m codexsync -c config.toml sync --apply

Guardian: защита глобального состояния

Guardian — единственная часть, которая работает при запущенном Codex. Он только читает .codex-global-state.json и пишет исключительно в собственный каталог, который валидация конфигурации обязана расположить вне .codex, папки синхронизации, backup и temp.

python -m codexsync -c config.toml guardian snapshot --once
python -m codexsync -c config.toml guardian watch
python -m codexsync -c config.toml guardian scheduler --platform windows --output-dir D:\codexSync\scheduler --log-dir D:\codexSync\logs

Как принимается решение:

  • три стабильных чтения подряд (интервал 3 с, debounce 2 с, резервный обход 60 с) — состояние, пойманное в момент записи, снимком не станет;
  • проверяются байты, JSON и ссылочная целостность: NUL, BOM, дубли ключей, битые ссылки между проектами и привязками;
  • подозрительное сокращение числа проектов или привязок уходит в карантин, а не заменяет последний хороший снимок;
  • порядок задаётся монотонным generation, а не часами; снимок виден только после COMMITTED, и ничто подозрительное не может стать latest-good.

Ограничение честно названо: snapshot --once по расписанию не гарантирует, что снимок окажется свежим на момент BSOD. Он гарантирует, что существующий снимок исправен и восстановим.

Перенос сессий между машинами

sessions scan сравнивает каждую ветку сессии на этой машине с копией в облачной папке и классифицирует: идентичны, fast-forward в ту или другую сторону, переход active ↔ archive, расхождение.

python -m codexsync -c config.toml sessions scan --source-machine desktop --target-machine laptop --save-plan sessions-plan.json
python -m codexsync -c config.toml sessions apply --plan sessions-plan.json --confirm-plan <точный-id-плана> --dry-run
python -m codexsync -c config.toml sessions apply --plan sessions-plan.json --confirm-plan <точный-id-плана>

Три правила несут всю безопасность и не обсуждаются:

  • Расхождение никогда не сливается. Ни чередования записей, ни сортировки по времени, ни «кто новее». Обе ветки остаются на диске побайтово, а план блокируется до вашего решения (sessions resolve).
  • Решение привязано к точным байтам обеих веток. Если любая из них потом изменилась, решение отклоняется как STALE_RESOLUTION.
  • Равенство записей определяют сырые байты. Каноническая форма JSON используется только там, где она заведомо однозначна, чтобы две разные записи не схлопнулись в одну.

Отчёт не содержит ни id сессий, ни имён тредов, ни содержимого записей: конфликт адресуется своим хешем.

Сжатие облачного зеркала

Облачная копия — собственный каталог codexSync, его не читает никакой Codex, поэтому ветка может лежать там в сжатом контейнере: semantic.mirror_compression принимает none, gzip или xz (по умолчанию xz: на реальном наборе из 250 сессий зеркало заняло 325 MiB против ~865 MiB локально, то есть около 38%). На .codex это не влияет никогда: ветка, записанная обратно в состояние Codex, всегда обычный JSONL.

Несущее правило: сжатие — свойство контейнера, а не истории. Хеш ветки, число записей и любое сравнение берутся из распакованного потока, поэтому сжатая копия сравнивается как IDENTICAL, а не как расхождение.

Настройка задаёт контейнер для ветки, которой в зеркале ещё нет. Ветка, которая там уже лежит, сохраняет свой контейнер (MIRROR_CONTAINER_KEPT): контейнер входит в имя файла, delete_policy = never, и два имени для одного id сессии сделали бы её невидимой для всех последующих планов.

Индекс сессий

session_index.jsonl — это журнал добавлений и обновлений, а не список существующих сессий: один id может встречаться на нескольких строках, у сессии может не быть строки вовсе, а строка может называть уже удалённый файл. Ничто из этого не ошибка, и codexSync это не «чинит».

python -m codexsync -c config.toml sessions index

Отчёт показывает, что лежит с каждой стороны и где они расходятся, не называя ни id сессий, ни имён тредов. Отдельно сообщается REDUCTION_AMBIGUOUS — случай, когда два правдоподобных прочтения повторяющегося id (последняя строка или наибольший updated_at) расходятся, что бывает ровно тогда, когда часы шли назад. Индекс не перезаписывается, пока поведение рантайма не подтверждено экспериментом; в отчёте это UNPROVEN_CONSUMER_CONTRACT.

Чаты и проекты

python -m codexsync -c config.toml chats tree
python -m codexsync -c config.toml chats list --text "парсер" --limit 20
python -m codexsync -c config.toml chats list --project none

Каждая строка говорит, почему чат находится там, где находится, и три причины ведут себя по-разному:

  • BOUND — явная привязка; следует за проектом, если тот переехал.
  • DERIVEDcwd чата попадает под корень проекта; переезд проекта такой чат теряет.
  • DERIVED_VIA_MAPPING — связь существует только благодаря правилу [[path_mappings]]. Codex эти правила не читает, поэтому в самом приложении такой чат не виден, пока его не привязать. Это список чатов, потерянных при переезде между машинами.

Перенос чата под проект — двухшаговый, как и всё остальное: сначала превью с id плана, затем то же самое с --confirm <id>. Файла плана нет: id считается по решениям и по точным байтам состояния, поэтому перестаёт совпадать, как только что-то изменилось.

Ремонт после переезда машины

Если проект переехал в другой каталог, repair-projects восстанавливает связь, опираясь на cwd из самих сессий и правила [[path_mappings]].

python -m codexsync -c config.toml repair-projects scan --source-machine desktop --target-machine laptop --save-plan repair-plan.json
python -m codexsync -c config.toml repair-projects apply --plan repair-plan.json --confirm-plan <точный-id-плана> --dry-run
python -m codexsync -c config.toml repair-projects apply --plan repair-plan.json --confirm-plan <точный-id-плана>

Доказательства для REMAP_ROOT намеренно узкие: существующий проект подходит только если его записанный корень, пропущенный через то же отображение, что и сессии, попадает ровно туда, куда теперь указывают эти сессии. Два кандидата — это AMBIGUOUS_PROJECT, а не выбор.

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

Пути миграции не переписываются внутри JSONL сессий — сырые байты являются идентичностью записи, и такая правка сделала бы одну историю на двух машинах навсегда расходящейся.

Прерванная мутация

Журнал, не дошедший до терминального состояния, блокирует любую следующую мутацию. Это защита, а не поломка, и выход из неё один:

python -m codexsync -c config.toml recover inspect <operation-id>
python -m codexsync -c config.toml recover resume <operation-id>
python -m codexsync -c config.toml recover rollback <operation-id> --target <путь>

rollback требует явного --target, потому что sync мог сделать резервные копии обеих сторон, а манифест хранит только относительные пути.

Политика конфликтов

conflict.policy принимает manual_abort, prefer_cloud, prefer_local, prefer_newer_mtime. При manual_abort конфликт вызывает ConflictError до любой записи.

Пути под sessions/, archived_sessions/, session_index.jsonl, глобальное состояние проектов и SQLite являются semantic-owned: они исключены из обычного копирования по mtime и обрабатываются только семантическими командами.

Коды выхода

Код Значение
0 успех
1 ошибка выполнения
2 обнаружен конфликт, нужно решение человека
3 Codex запущен (не выполнено условие холодной операции)
4 неверная конфигурация или аргументы
5 безопасная остановка (fail-safe)

doctor/preflight возвращают 0, если все проверки прошли или есть только предупреждения, и 5, если хотя бы одна проверка провалена.

Обязательный протокол работы

  1. Закрыть Codex на машине A.
  2. Дождаться полной выгрузки изменений облачным клиентом.
  3. Запустить codexSync на машине B.
  4. Запускать Codex на машине B только после завершения синхронизации.
  5. Заново войти в Codex на машине B.

Токены аутентификации codexSync не переносит — это ограничение лицензии OpenAI.

Проект намеренно не проверяет состояние облачного провайдера, процесс облачного клиента и свободное место: это ответственность пользователя.

Резервные копии и логи

Резервные копии складываются в paths.backup_dir со снимками, несущими проверяемый манифест codexsync-backup-v1. Хранение ограничено backup.retention_days и backup.max_backups.

Единственное исключение — conflict bundle: проигравшая ветка расхождения хранится в semantic.root_dir, которого retention не касается, потому что иначе единственная копия расходящейся истории исчезла бы по таймеру.

Логи настраиваются в секции [logging]; каждое опасное действие логируется отдельно (создана резервная копия / перезапись / пропуск).

Платформы и CI

CI гоняет pytest на windows-latest и macos-latest для Python 3.11, 3.12 и 3.13. Linux в MVP отключён намеренно: официальной доступности Codex для Linux нет, поэтому версии не зафиксированы.

Что осталось неподтверждённым

Две возможности намеренно бездействуют, пока контролируемый эксперимент на расходуемом состоянии не зафиксирует реальное поведение рантайма Codex:

  • запись перенесённой ветки внутрь .codex (BLOCKED_UNPROVEN_LAYOUT);
  • перезапись session_index.jsonl (UNPROVEN_CONSUMER_CONTRACT).

Обе сообщаются, а не угадываются. Протоколы — в docs/experiments.

Графический интерфейс

Версия 0.2 — выпуск командной строки. Необязательный extra codexsync[gui] существует как заготовка и не является готовым интерфейсом.

Статус

0.2 — выпуск командной строки: Guardian, единый защитный конвейер для всех мутаций, ремонт после переезда машины, семантический перенос сессий и чаты.

Публикация и лицензирование

Чек-лист релиза: docs/PUBLISHING.md. История изменений: CHANGELOG.md.

Проект под двойной лицензией:

Вклад принимается на условиях CONTRIBUTING.md и CLA.md.