← Блог·(обновлено: 11 сентября 2026 г.)1103584

Почему Codex съедает недельный лимит за день, пока агент ждёт у моря погоды

Это история о том, как мы нашли утечку входных токенов у моделей, работающих в среде Codex. Утечка живёт до сих пор, и из-за неё толпы людей в X недоумевают, почему недельный лимит подписки за 200 долларов расходуется за день. Как часто бывает в программировании, искрой стала мелкая ошибка вокруг целых и дробных чисел, но это лишь искра и малая часть, на самом деле всё интереснее. Приглашаем к чтению: на осознанное понимание уйдёт около 30 минут.

TL;DR

  • Наш оркестратор на Codex ждал дочерних агентов. Модель muse-spark 1.3 через провайдера opencode (кудос команде Muse, 1.3 уже сильная модель). За 48 минут goal-режим запустил 173 продолжения, каждое перечитывало от 120 до 470 тысяч токенов контекста, пока провайдер не ответил 429. Дальше эта сессия называется исходной.
  • Причина: goal-режим перезапускает модель через 0.03 секунды после каждого хода и засчитывает ожидание только за живой опрос процесса, а инструмент паузы clock.sleep выдан только недавно вышедшей gpt-6-astra. Ожидание превращается в спин: каждая проверка стоит полный контекст.
  • Цена: из 3808 сессий с июля по сентябрь 2026 года 52 с goal-режимом съели половину из 59.2 миллиарда входных токенов. Час ожидания стоил от 83 до 188 миллионов. Недельный лимит Pro-аккаунта за 200 долларов кончался пятнадцать раз за лето, в медиане через 23 часа после начала окна.
  • Что делать: самое простое, брать gpt-6-astra на минимальном усилии как нижнюю планку для goal-задач, это единственная модель с sleep из коробки. Либо включить sleep через конфиг для остальных, ждать все дочерние процессы одним вызовом и ставить бюджет на цель. Второй путь мы у себя внедряем, но на масштабе ещё не проверили. Со стороны Codex: выдавать sleep при активном goal и делать паузу между продолжениями, как это устроено в Claude Code.
  • Все цифры по Codex 0.154 и роллаутам за июль, август и сентябрь 2026. Каждое утверждение про механику подкреплено ссылкой на код Codex на GitHub, сами роллауты не публикуются.

Как считаются токены

Языковая модель не помнит разговор. Каждый раз, когда ей нужно сделать шаг, ей заново отправляют всю переписку целиком, и она отвечает одним действием: вызовом инструмента или текстом. Если переписка занимает 240 тысяч токенов, то каждый шаг стоит 240 тысяч входных токенов. Неважно, что сам шаг это “выполни tail -n 18”. Платим за объём, который модель перечитывает на каждом шаге. Значит, любая работа стоит число шагов, умноженное на размер контекста, и дешевле её делают либо меньше шагов, либо меньше контекст.

Короткий экскурс в KV-кэш

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

В агентской сессии почти каждый запрос это предыдущий запрос плюс несколько новых строк. Поэтому кэш попадает почти всегда. В проанализированном архиве из кэша пришло 97.8% входных токенов.

Провайдеры отражают это в цене. У OpenAI кэшированный вход стоит 10% от обычного, кэширование включается автоматически на промптах длиннее 1024 токенов, см. документацию по prompt caching. Для оплаты через API это хорошая новость: почти весь перечитываемый контекст идёт по льготной цене.

А что с подпиской

У подписки ChatGPT единица учёта это доля лимита в скользящем окне. Формула расчёта этой доли публично не описана. Клиент Codex получает от сервера только процент использования и время сброса, это видно в протоколе в структуре RateLimitWindow. Учитывается ли там скидка за кэш, снаружи не проверить. В той же документации по кэшированию OpenAI прямо говорит, что для лимитов API кэшированные токены считаются как обычные. Единственное, что мы знаем про подписку по опыту: сессии, где 98% входа было из кэша, всё равно выбивали недельный лимит. Поэтому дальше я считаю входные токены целиком, без скидки: похоже, именно они решают, когда вы упрётесь в стену.

Что значит “подождать”

Агент часто должен дождаться чего-то внешнего: сборки, тестов, другого агента. Есть два способа это делать.

Уведомление. Агент делает один шаг “сообщи мне, когда закончится” и отпускает ход. Пока ждём, шагов нет, значит нет и расходов. Когда событие случилось, приходит одно сообщение, и это один новый шаг.

запустил → отпустил ход → тишина, ноль токенов → "готово" → один шаг

Опрос. Агент сам периодически проверяет: не закончилось ли? Каждая проверка это шаг, каждый шаг это полный контекст.

проверил → 240k → "нет" → подождал → проверил → 240k → "нет" → ...

Отсюда формула, вокруг которой построен весь текст:

стоимость ожидания = число проверок × размер контекста

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

Известная пауза и неизвестная

Простой тест. Попросите модель: “напиши раз, а через пять секунд напиши два”. Она сделает это правильно и дёшево: один вызов sleep 5 в шелле, команда заблокирует ход на пять секунд, вернётся, модель напишет “два”. Один шаг, один раз оплачен контекст.

Теперь попросите: “дождись, пока закончится сборка”. Длительность неизвестна. Может быть минута, может быть три часа. Один sleep на три часа модель сделать не может: если сборка закончится через минуту, она проспит результат, а если не закончится, упрётся в потолок длины одной команды. Поэтому она выбирает интервал, скажем 30 секунд, и делает цикл: поспать, проверить, поспать, проверить.

известная пауза:    1 sleep                 = 1 шаг
неизвестная пауза:  N × (sleep + проверка)  = 2N шагов

Пять секунд модель ждёт правильно, потому что ей сказали, сколько. Для неизвестной паузы нужен другой примитив: “разбуди, когда случится”. Всё дальнейшее про то, есть ли он у модели.

Если говорить языком async/await

Программистам (сорри, вайбкодеры) проще думать про это как про асинхронную задачу. Ход модели это запуск задачи, конец хода это точка, где задача отдаёт управление рантайму. Способов дождаться внешнего события три, и все знакомы по асинхронному коду.

// 1. await: задача приостановлена, продолжение хранит рантайм
let result = await child.finished     // ноль вызовов, пока ждём
handle(result)                        // один вызов по событию

// 2. sleep-опрос: приостановка на фиксированный интервал
while !child.finished {
    await sleep(45s)                  // один вызов на каждый интервал
}

// 3. спин: задача не умеет приостанавливаться
while !child.finished {
    poll()                            // вызов за вызовом, без пауз
}

В обычном коде спин-ожидание просто греет одно ядро. У агента оно устроено хуже: память модели это её контекст, и каждая итерация спина перечитывает всю эту память заново. Это как цикл while, в котором на каждом обороте копируется вся куча процесса.

Одна минута ожидания тремя способами: await, sleep-опрос, спин

Какой из трёх вариантов достаётся модели, решает диспетчер, который запускает её ходы, и набор инструментов, который ей выдали. Дальше про то, что выдаёт Codex.

Как ждёт Codex

У Codex с недавних пор есть нужный инструмент. Он называется clock.sleep: “поспи столько-то миллисекунд, разбуди раньше, если что-то пришло”. Появился он в PR 28429 и вышел в 0.141.0 от 18 июня 2026, под именем clock.sleep живёт с 0.143.0 от 8 июля. Реализация в sleep.rs делает ровно это: засыпает до 12 часов и прерывается на новый ввод. Это второй вариант из списка выше, и для ожидания он почти так же хорош, как первый.

Только модель его не получает. Инструмент регистрируется по правилу из spec_plan.rs: он есть, если в каталоге модели указана поддержка clock, либо если его включили в конфиге принудительно. Во встроенном каталоге моделей clock есть у одной модели, gpt-6-astra. У gpt-5.6-sol, gpt-5.5, gpt-5.4 и у всех моделей с кастомных провайдеров список пуст. В codex features list при этом написано sleep_tool stable true, но это флаг фичи, а не факт наличия инструмента в запросе. В архиве из 3808 сессий sleep вызывался 420 раз, и 418 из них сделала gpt-6-astra.

Почему инструмент выдан одной модели, в исходниках не написано, но подсказки есть. Поле в каталоге называется experimental_supported_tools, а режим по умолчанию подписан в коде как “сохранить существующие дефолты модели”. Это похоже на поэтапный выкат: сам инструмент работает на клиенте и ничего от модели не требует. У gpt-6-astra в том же поле стоит второй экспериментальный инструмент, send_user_message_async, который позволяет писать пользователю, не заканчивая ход. Вместе они описывают модель, которую учили работать в длинных фоновых сессиях: ждать, просыпаться, докладывать по дороге. Похоже, это новая линия поведения, которую измерили на новой модели и не стали приписывать старым. Причина для осторожности есть: модель, не обученная выбирать длительность сна, может заснуть вместо работы или крутить sleep 1 в цикле. В этой логике есть дыра: goal-режим включён для всех моделей, а инструмент, без которого goal-режим превращается в спин, только для одной.

Что остаётся без sleep

Единственный способ подождать это выполнить команду в шелле: sleep 30, tail по логу, что угодно. Это шаг, а значит полный контекст. И у этого шага есть потолок длины. Шелл в Codex работает через unified exec: команда запускается в псевдотерминале, а инструмент отдаёт управление модели через yield_time_ms. По умолчанию это 10 секунд, максимум 30, см. константы в unified_exec/mod.rs и дефолтное значение в handlers/unified_exec.rs. Если процесс ещё жив, модель получает идентификатор сессии, и дальше может дочитывать его вывод через write_stdin с пустым вводом. Такой пустой вызов умеет ждать дольше, до пяти минут. То есть даже без sleep у модели есть законный способ платить один вызов за пять минут ожидания.

В исходной сессии этот путь был закрыт. muse-spark работала через кастомного провайдера и передавала числа в аргументах инструментов как дробные: 60000.0 вместо 60000. Парсер Codex ждёт целое и отвергает вызов. В архиве это видно по всем сессиям на этой модели: 10 из 10 вызовов write_stdin и 23 из 25 вызовов с yield_time_ms упали с ошибкой вида invalid type: floating point 60000.0, expected u64. Заодно это значит, что принудительно включённый sleep ей не поможет: его аргумент duration_ms тоже целое.

Дрейк отворачивается от 60000 и одобряет 60000.0

Потолок в 10 секунд отсюда и берётся. Когда модель запускает команду, Codex отдаёт ей управление по таймеру yield_time_ms, по умолчанию через 10 секунд. Продлить таймер можно только числом в аргументе, а числа у этой модели не проходят. Пустой write_stdin, который умеет ждать до пяти минут, отваливается по той же причине. Значит, одно обращение к шеллу никогда не длится дольше 10 секунд, что бы модель ни написала.

На практике модель до этого потолка даже не доходила. Её опрос был снапшотом: команда читала журнал событий дочернего процесса и возвращалась за секунду. Остальное время хода уходило на два вызова модели, один чтобы запустить опрос, второй чтобы написать “без изменений”. Медиана хода десять секунд, среднее семнадцать с учётом более длинных ходов, и goal тут же запускает следующий. За 48 минут набралось 173 таких хода и 466 вызовов модели, каждый вызов перечитывал от 120 до 470 тысяч токенов. Итого 149 миллионов входных токенов за 48 минут, или 188 миллионов в час.

Для сравнения, у официальной gpt-5.6-sol тот же цикл в сессии A стоил 83 миллиона в час: там модель ждала по 30 секунд за вызов, это потолок для запущенной команды. В сессии B 20 миллионов: модель дочитывала вывод пустым write_stdin и получала по пять минут паузы за вызов. Чем длиннее пауза внутри одного вызова, тем дешевле час. Исходная сессия сняла с цикла последний тормоз, и именно так баг стал виден.

Что добавляет goal-режим

Без goal у модели есть запасной выход: закончить ход и замолчать. Даже без sleep это даёт бесплатную паузу. Человек вернётся, напишет, тогда продолжим. На языке async это первый вариант, await: задача отдала управление, и никто её не будит, пока не случится событие.

Goal-режим убирает и этот выход. Как только тред становится свободен, goal-расширение немедленно отправляет модели новый ход “продолжай работать над целью”. Это видно в extension.rs: обработчик on_thread_idle вызывает continue_if_idle из runtime.rs, а тот стартует ход сразу. Никакого бэкоффа, никакого счётчика пустых продолжений. Бюджет токенов на цель по умолчанию не задан.

Ральф Виггам в автобусе: (chuckles) I’m in danger

Ральф здесь не случайно: любой goal-режим это по сути Ralph loop, while true; do codex "продолжай"; done, только встроенный в продукт. И лицо у него соответствующее.

Слово “немедленно” здесь измеримо. В исходной сессии между концом одного хода и началом следующего проходило от 0.02 до 0.05 секунды, медиана 0.03. Ход при этом обычно длился десять секунд: ровно столько, сколько нужно модели, чтобы прочитать контекст, вызвать один опрос и написать “без изменений”. Частоту опроса задавала задержка ответа модели, ничего больше.

Две с половиной минуты исходной сессии: ходы по 8–27 секунд с зазорами 0.03 секунды

На этой картинке есть важный момент. В 23:50 я спросил модель, почему она так часто проверяет. Модель ответила: “Контракт гола требует каждый ход либо двигать работу, либо доказывать verified wait живым опросом ранов”, и пообещала опрашивать реже. Через 0.3 секунды после её ответа goal-режим запустил следующий ход. Обещание не могло быть выполнено: модель не управляет тем, когда её вызовут в следующий раз.

Текст того самого контракта лежит в continuation.md. В нём есть абзац про ожидание. Ожидание засчитывается, только если в этом ходу модель опросила живой процесс. “Я жду” без опроса считается отсутствием прогресса. Модель сама может перевести цель только в complete или blocked, причём blocked разрешён после трёх одинаковых блокеров подряд, а ожидание дочерних процессов блокером не считается. Поставить цель на паузу может только человек.

Получается ловушка:

  • закончить ход и подождать нельзя, тебя тут же перезапустят;
  • перезапуск засчитается как “нет прогресса”, если не опросить процесс;
  • значит, каждый ход надо что-то опросить;
  • каждый опрос стоит полный контекст.

Модель выполняет требование буквально. Требование сформулировано так, что поведение, правильное по контракту, оказывается самым дорогим по токенам. В терминах async goal-расширение это диспетчер с одним правилом: очередь пуста, поставь ту же задачу снова. А контракт запрещает задаче приостанавливаться иначе как через опрос. Вместе это спин, третий вариант из списка.

Как та же идея устроена в Claude Code

У Claude Code есть тот же режим, он тоже называется /goal: задаёшь условие, и Claude работает, пока оно не выполнено. Отличается диспетчер. По документации, после каждого хода условие проверяет отдельная маленькая модель, и только тогда стартует новый ход. Если фоновая команда или сабагент ещё работают, проверка откладывается, а их результат приходит новым ходом. Когда фоновая работа тянется долго, первая проверка идёт через 30 минут, дальше интервал удваивается, и без человека проверок не больше трёх. Несколько ходов подряд без единого вызова инструмента останавливают цикл с предупреждением. В Codex на каждый из этих случаев ответ один: новый ход через 0.03 секунды.

Строка про фоновую работу здесь главная. Это и есть await из первого варианта: задача отдала управление, диспетчер держит продолжение и будит её по событию. Сами фоновые примитивы тоже есть: у Bash есть фоновый режим с уведомлением по завершении, у сабагентов тоже, а Monitor следит за потоком событий и будит модель на каждой строке вывода. Документация по /loop прямо советует использовать Monitor вместо опроса по таймеру, потому что это дешевле по токенам.

Поэтому в сессиях Claude Code такого цикла не видно: диспетчер умеет ждать события и замедляться, когда ждать приходится долго. У Codex диспетчер умеет только перезапускать.

Обучение моделей здесь ни при чём. В Claude Code за фоновым процессом следит обёртка: когда он завершается, она кладёт в разговор сообщение “задача закончилась” и вызывает модель. У Codex похожие примитивы тоже есть, очередь треда и sleep с прерыванием по вводу, но завершение шелл-команды сообщением не становится, модель должна сама дочитывать вывод. Вся разница в том, кто ждёт: обёртка, которая будит модель, или модель, которая спрашивает сама.

Исходники Claude Code закрыты, поэтому механику мы смотрели по ClawCodex, стороннему порту на Python, который переносит TypeScript-референс файл за файлом с номерами строк оригинала. Путь уведомления там короткий: background.py дожидается процесса и кладёт конверт <task-notification> в очередь, agent_server.py сливает очередь между ходами и отдаёт модели одним ходом. Пока процесс идёт, модель не вызывают. Goal-режим в порте воспроизведён не полностью, для него источником остаётся документация.

Пустые ходы

Есть и более простой вариант той же ловушки. Модель заканчивает ход, ничего не сделав, например, написала “жду”. Goal тут же перезапускает её. Она снова ничего не делает. Снова перезапуск. В логах есть цепочки по четыре таких хода за 20 секунд. В одной сессии четырнадцать пустых ходов стоили 77 миллионов входных токенов: ничего не произошло, а контекст был перечитан четырнадцать раз. Именно от этого случая Claude Code защищается остановкой после нескольких ходов без вызова инструментов.

Почему это особенно бьёт по оркестраторам

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

С одним дочерним процессом всё ещё дёшево. Есть команда, которая блокирует ход, пока процесс не закончит, и это тот же случай, что sleep 5: один шаг, только длительность задаёт не таймер, а событие. Потолок одного вызова в Codex около пяти минут, столько умеет ждать пустой write_stdin, поэтому трёхчасовой процесс обойдётся примерно в 36 вызовов подряд. Это 12 шагов в час, а не 270.

С двумя и больше всё меняется. Пока оркестратор заблокирован на процессе A, процесс B может закончиться, упасть или попросить помощи, и оркестратор этого не увидит. Если бы ему нужен был только факт “оба закончили”, он мог бы ждать A, потом B, и это по-прежнему дёшево. Но оркестратору нужно реагировать по ходу: принять результат, снять блокировку, перезапустить упавшего. Поэтому он не решается на долгую блокировку и заменяет её коротким циклом: посмотреть A, посмотреть B, подождать, снова. Итог: один дочерний процесс это известное ожидание, N процессов без общего барьера это поллинг. В сессиях ниже это видно буквально: оркестратор знал про блокирующее ожидание и использовал его 31 раз, но между блокировками всё равно ходил по логам обоих процессов.

То же самое происходит в любом паттерне, где агент запускает несколько долгих процессов и не может получить оповещение: несколько CI-прогонов, сборка и деплой параллельно, несколько удалённых задач. Оркестратор с дочерними процессами это просто самый частый случай. У Codex есть свой барьер по нескольким дочерним агентам, инструмент wait_agent с описанием “передай несколько идентификаторов, чтобы дождаться того, кто закончит первым”, см. multi_agents_spec.rs. Но он видит только агентов, которых сам Codex породил через spawn_agent. Дети нашего оркестратора для него не существуют.

Что было у модели в исходной сессии

Вот способы подождать, которые в теории есть у модели в Codex, и что с каждым случилось в исходной сессии.

Способ подождать Что случилось
clock.sleep Не выдан: у модели с кастомного провайдера нет clock в каталоге
wait_agent, барьер по нескольким дочерним агентам Дочерние процессы не Codex-агенты, инструмент их не видит; в следующей сессии той же ночи попытка нативного spawn_agent вернула unsupported call
write_stdin с пустым вводом, до 5 минут за вызов Единственная попытка упала: floating point 36429.0, expected i32
yield_time_ms длиннее 10 секунд Единственная попытка упала: floating point 60000.0, expected u64
sleep в шелле Десять раз по 4–8 секунд; всё, что длиннее 10 секунд, обрезал бы unified exec
Блокирующий флаг у команды наблюдения Ни разу не использован, но он и не помог бы: тот же потолок 10 секунд
Закончить ход и подождать Новый ход через 0.03 секунды

Пять из семи дверей были закрыты средой, одну модель не нашла, и одна была заперта контрактом. Осталось то, что мы видели в логе: один опрос на ход. Из 181 хода сессии 135 состояли ровно из одной такой команды.

Исходная сессия: 544 вызова модели, входные токены каждого вызова, goal-цикл выделен

Эта картинка заодно даёт естественный эксперимент. Пока goal активен, оранжевые столбцы идут сплошной стеной, и каждый выше предыдущего, потому что контекст растёт. В 00:03 провайдер отвечает 429, ход падает с ошибкой, и цикл замирает на час, пока я не вернулся. В 01:34 я возобновляю цель, и опрос начинается снова, каждые 12 секунд. В 01:35 нажимаю Ctrl-C, цель встаёт на паузу, и через восемь минут говорю “продолжим без goal”. Дальше пять вызовов, работа сделана, сессия закончилась. Контекст, модель и задача те же, менялся только диспетчер.

Что показали сессии

Я взял все роллауты Codex с двух машин за июль, август и сентябрь: 3808 файлов, каждый это один тред, включая дочерние. В них 59.2 миллиарда входных токенов и 129 миллионов выходных, из кэша пришло 97.8%. Версии CLI от 0.144 до 0.154, модели в основном gpt-5.6-sol, плюс terra, luna, gpt-5.5, gpt-5.4, gpt-6-astra, gpt-5.3-codex-spark, а также Qwen 3.6 и muse-модели через кастомных провайдеров.

Goal-режим был включён в 52 сессиях из 3808, это полтора процента. Вот сколько они весят.

52 goal-сессии занимают 49% входных токенов из 3808 сессий

Полтора процента сессий съели половину токенов: 29.2 миллиарда из 59.2. Семь самых дорогих сессий архива, от 1.8 до 5.7 миллиарда каждая, все с goal-режимом. Самая тяжёлая сессия без него весит 1.2 миллиарда. Без принудительного продолжения модель заканчивала ход и ждала человека, а это бесплатно.

А вот что это значило для лимита. Каждое событие token_count в роллаутах несёт снапшот лимитов аккаунта, и по нему видно: за десять недель с июля по сентябрь недельное окно тринадцать раз подходило к 99% и одиннадцать раз упиралось в 100%, ошибка “You’ve hit your usage limit” встречается в 28 календарных днях. От начала окна до 99% проходило в медиане 25 часов, самый быстрый случай 7 часов, самый долгий 93. Пятичасовое окно за то же время кончалось дважды, упор почти всегда шёл в недельное. При 83–188 миллионах входных токенов в час, которые стоит ожидание в сессиях ниже, сутки дают от двух до четырёх миллиардов. Отсюда и “за день” в заголовке: это медиана.

Вот одна из goal-сессий по ходам. Синие ходы начал человек или сам оркестратор, оранжевые запустил goal-режим.

Сессия A: входные токены на каждый ход, goal-продолжения выделены

Оранжевые столбцы это часы опроса логов. Самый высокий, 71 миллион токенов, это один ход длиной три часа с 439 вызовами инструментов, в основном tail по логам дочерних процессов. Справа четыре почти невидимых оранжевых столбца это пустые продолжения за 20 секунд.

Главная цифра: сколько стоит один час ожидания при сопоставимом контексте.

Стоимость часа ожидания: от 0.5 миллиона с уведомлением до 188 миллионов в goal-цикле без sleep

Сессия A ждала 7.8 часа и потратила на это 650 миллионов входных токенов, 271 шаг в час. Сессия B ждала 92 часа и потратила 1.85 миллиарда, 43 шага в час, в ней модель делала sleep 30 через шелл 61 раз. Исходная сессия за 48 минут goal-цикла сделала 466 вызовов, 590 в час, и потратила 149 миллионов. Верхняя полоска это оценка для того же контекста с уведомлением: один-два шага на событие.

Между ними лежит измеренный случай с sleep. В архиве есть две goal-сессии на gpt-6-astra, единственной модели, которой инструмент выдан. Модель пользовалась им, 60 и 129 раз, спала по 45 секунд за вызов. Час ожидания стоил ей 10–15 миллионов входных токенов при 55–100 вызовах. Это в 6–18 раз дешевле спина, и всё же в 20–30 раз дороже уведомления: goal-режим по-прежнему перезапускал её после каждого хода, 600 и 443 раза за сессию, а спала она недолго. Sleep снижает цену цикла, но не убирает сам цикл.

Ещё одна деталь из данных. Qwen 3.6 через локальный llama.cpp показал тот же рисунок, 21 ход из 22 в goal-сессии были чистым опросом. Инструмент паузы гейтится по каталогу модели, провайдер тут ни при чём, и одной muse-моделью дело не ограничивается.

Что с этим делать

Со стороны Codex это несколько маленьких правок, и у всех есть готовый образец в соседнем продукте.

  • Регистрировать clock.sleep, когда у треда активный goal, независимо от каталога модели. Слабее, но тоже работает: сделать режим always_on дефолтом при включённых goals.
  • Добавить выдержку между продолжениями, когда предыдущий goal-ход был ходом ожидания или пустым ходом. Claude Code делает это через 30 минут с удвоением интервала и потолком в три автопроверки, и останавливает цикл после нескольких ходов без инструментов. Сейчас continue_if_idle стартует ход сразу.
  • Принимать 60000.0 там, где ожидается 60000. Модели с кастомных провайдеров пишут числа так, и из-за этого у них не работает ни один инструмент ожидания.
  • Одна строка в continuation.md: назвать clock.sleep и долгий write_stdin допустимыми формами verified wait, иначе даже модель с инструментом пойдёт опрашивать шелл.

Как проверить свои сессии

Все таблицы из этого текста можно получить по своим роллаутам одним скриптом: codex-rollout-audit, один файл на Python без зависимостей. Он читает ~/.codex/sessions, ничего никуда не отправляет и печатает сводку по сессиям, эпизоды исчерпания лимитов, разбор одной сессии по ходам и счётчик отклонённых дробных аргументов.

Как включить sleep у себя

Флаг живёт в ~/.codex/config.toml:

[features]
sleep_tool = { mode = "always_on" }

Ключ описан в feature_configs.rs, режим always_on регистрирует инструмент независимо от каталога модели. После перезапуска Codex clock.sleep попадает в набор для любой модели. Проверить проще всего, попросив модель поспать пять секунд через clock.sleep и посмотрев, появился ли вызов в ответе. Три оговорки. На gpt-6-astra ничего включать не нужно: у неё clock объявлен в каталоге, и в режиме по умолчанию инструмент регистрируется сам. Для остальных моделей флаг только добавляет инструмент в набор. Пользоваться им разумно модель не учили: она может продолжить опрашивать шелл или уснуть вместо работы. Для моделей с кастомных провайдеров, которые пишут 60000.0 вместо 60000, инструмент не заработает, пока не починена сериализация чисел.

У себя мы включили sleep через конфиг, учим оркестратор ждать все дочерние процессы одним вызовом и ставим бюджет на каждую цель. Формула та же: число проверок на размер контекста. Пока модели не дали способ ждать, не тратя шаг, платить будет тот, кто ждёт.