Утилита с открытым исходным кодом для переноса локального состояния 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.
- Определяется, запущен ли Codex. Ответ «не удалось определить» считается «запущен»: ничего мутирующего не делается оптимистично.
- Команды только на чтение (
doctor,plan, любыеscan,chats,guardian) работают в любом случае. Результат, полученный при открытом Codex, помечаетсяvolatileи не может быть переиспользован мутацией. - Мутация выполняется только при закрытом Codex и всегда одним конвертом:
неперехватываемая блокировка → долговечный журнал → проверенный backup всего,
что будет заменено → финальная проверка процесса непосредственно перед
коммитом → staging на том же томе → атомарная замена →
COMMITTED. - Любая неоднозначность останавливает операцию, а не разрешается догадкой, и код выхода говорит, какого рода была остановка.
Запуск из корня проекта:
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 --applyGuardian — единственная часть, которая работает при запущенном 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— явная привязка; следует за проектом, если тот переехал.DERIVED—cwdчата попадает под корень проекта; переезд проекта такой чат теряет.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, если хотя бы одна проверка провалена.
- Закрыть Codex на машине A.
- Дождаться полной выгрузки изменений облачным клиентом.
- Запустить codexSync на машине B.
- Запускать Codex на машине B только после завершения синхронизации.
- Заново войти в Codex на машине B.
Токены аутентификации codexSync не переносит — это ограничение лицензии OpenAI.
Проект намеренно не проверяет состояние облачного провайдера, процесс облачного клиента и свободное место: это ответственность пользователя.
Резервные копии складываются в paths.backup_dir со снимками, несущими
проверяемый манифест codexsync-backup-v1. Хранение ограничено
backup.retention_days и backup.max_backups.
Единственное исключение — conflict bundle: проигравшая ветка расхождения
хранится в semantic.root_dir, которого retention не касается, потому что иначе
единственная копия расходящейся истории исчезла бы по таймеру.
Логи настраиваются в секции [logging]; каждое опасное действие логируется
отдельно (создана резервная копия / перезапись / пропуск).
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.
Проект под двойной лицензией:
- открытая лицензия
GPL-3.0-or-later— см. LICENSE; - коммерческий путь — см. COMMERCIAL_LICENSE.md.
Вклад принимается на условиях CONTRIBUTING.md и CLA.md.