Octofs 0.14: ожидание — не вызов инструмента
Агент запустил тестовый набор. Тестовый набор идёт четыре минуты. Таймаут простоя у MCP-клиента — шестьдесят секунд.
Чем это кончится, понятно заранее. На шестидесятой секунде клиент отменил вызов. Процесс продолжил работать — останавливаться ему никто не велел, — а модель, получив отмену там, где должны были быть результаты тестов, поступила разумно: запустила набор ещё раз. Два тестовых прогона в одном каталоге, наперегонки за одни и те же артефакты сборки. Второй упал с ошибкой блокировки, модель отчиталась, что тесты сломаны, — а тесты были в порядке.
В другой сессии та же модель, уже обжёгшись, выработала обходной манёвр: запустить сборку, затем вызвать sleep 240, затем посмотреть. Вызов инструмента, который не делает ничего и висит открытым четыре минуты — ради того, чтобы другому вызову было что показать. Модель заново изобрела опрос по таймеру, причём плохо, потому что ничего лучше мы ей не дали.
В прошлый раз мы писали об octofs про правки, попадающие не в ту строку. Тот пост заканчивался принципом: настоящий интерфейс MCP-сервера — каждая строка, которую он отдаёт модели. Одиннадцать релизов с тех пор — с 0.10.1 по 0.14.1, две недели — применяют тот же принцип к самой медленной из этих строк: той, которую модель ждёт. Shell теперь событийный. Команды стартуют на переднем плане, сами уходят в фон, если живут дольше десяти секунд, а клиент получает уведомление, когда они завершаются. Ничто не блокируется, ничто не убивается, ничто не запускается дважды.
Вот как мы к этому пришли — включая один неверный поворот.
Первое исправление: доказать, что вызов жив
У отмены на шестидесятой секунде была поверхностная причина и глубокая. Поверхностная: вызов shell молчалив по своей природе. Компилирующаяся сборка минутами не передаёт по проводу ни байта, а для MCP-клиента молчание неотличимо от зависшего сервера. Поэтому 0.10.2 добавил heartbeat-сигналы жизни: пока команда работает на переднем плане, octofs шлёт уведомление о прогрессе каждые десять секунд — с большим запасом до любого разумного таймаута простоя, так что даже один пропущенный удар не приведёт к отмене вызова.
Убийства прекратились. Глубокую проблему это не тронуло: вызов по-прежнему блокировал. Четырёхминутный тестовый набор по-прежнему стоил четырёх минут сессии, в которые модель не могла ничего — ни прочитать упавший файл, ни подготовить следующую правку, ни подумать. Heartbeat делает ожидание переживаемым. Полезным он его не делает.
Второе исправление: фоновые задачи — и флаг, который пришлось удалить
0.11.0 ввёл фоновое выполнение: запускаете команду как задачу, немедленно получаете обратно дескриптор, вывод забираете позже. Каждая задача — это MCP-ресурс с URI вида octofs://jobs/17342-1, который в любой момент можно прочитать и узнать её статус и вывод.
Релиз вышел с флагом background у инструмента shell, и задним числом мы узнаём в этом флаге знакомую ошибку. В 0.9.0 мы удалили переключатель --line-mode, потому что безопасность, спрятанная за флагом, — это безопасность, которую большинство никогда не включит. Флаг background был тем же багом в другом костюме: он просил модель предсказать длительность команды до её запуска. Модели плохи в этом ровно так, как вы и ожидаете: cargo build на тёплом кеше мгновенен, а на холодном идёт шесть минут, и флаг превращал этот непознаваемый факт в обязательное решение. Угадаете «фон» для быстрой команды — добавили бессмысленный лишний круг. Угадаете «передний план» для медленной — и вы снова у заблокированного вызова, с которого мы начинали.
Поэтому 0.13.0 удалил флаг и заменил предсказание измерением. Каждая команда стартует на переднем плане. Если через десять секунд она всё ещё работает, она автоматически повышается до фоновой задачи — тот же самый процесс, не убитый и не перезапущенный. Захват вывода надёжен с первого байта, так что пересечение границы не теряет ничего: всё, что команда успела напечатать до ухода в фон, лежит в логе задачи и ждёт, когда вы его прочитаете.
В момент повышения вызов инструмента немедленно возвращается — со ссылкой на ресурс, в имени которой лежит сама команда, — так что клиент может отрисовать «make test … ещё выполняется», не восстанавливая заново, что это была за задача, даже после уплотнения контекста. Когда процесс завершается, octofs шлёт notifications/resources/updated для URI задачи. Клиент один раз читает ресурс и получает код выхода и хвост вывода. Ни опроса, ни удерживаемого открытым вызова, ни осиротевшего процесса.
Две детали здесь заслужили своё место дорогой ценой:
- Хвост, а не голова. Чтение ресурса возвращает не больше последних 30 КБ вывода. Логи сборки длинные, а вердикт — ошибка, итоговая сводка тестов — живёт в конце. Скормите модели первые 30 КБ лога, последняя строка которого гласит
FAILED, — и получите уверенный отчёт о том, что всё прошло успешно. - Два пути доставки. Клиенты на ревизии MCP от 2026-07-28, открывшие поток подписки, получают завершение в нём; более старые клиенты получают незапрошенный push, который допускала прежняя спецификация. А с 0.13.0 клиент, подписавшийся с опозданием — уже после того, как задача завершилась, — получает завершение заново, вместо того чтобы вечно ждать уведомления, которое прозвучало, когда его ещё никто не слушал.
Окно переднего плана в 0.13.0 тоже сжалось — с тридцати секунд до десяти, и это автоповышение окупает само себя: когда пересечение границы не стоит ничего — тот же процесс, надёжный вывод, уведомление в конце, — нет причин держать сессию в заложниках полминуты на случай, если команда вдруг закончится на двадцать пятой секунде.
Третье исправление: пусть задачи работают бок о бок
0.11.0 был консервативен: одна задача на каталог, точка. Безопасно — и слишком грубо: сборка и чтение хвоста лога выстраивались в очередь, хотя ждать друг друга им было совершенно незачем.
0.14.0 сузил ограждение до единственного случая, который действительно баг: та же самая команда уже выполняется в том же каталоге. Это не параллелизм — это дважды запущенный тестовый набор из истории в начале поста, и вместо гонки octofs отклоняет дубликат и говорит модели ровно, что делать:
The same shell command is already running as background job
octofs://jobs/17342-1 (`cargo test`). Wait for its completion — you will
get a resources/updated notification with its output — instead of
starting a duplicate. Independent commands may run concurrently in this
directory.
Разные команды работают бок о бок. Дубликат получает ошибку, которая — снова — и есть инструкция по восстановлению.
И жёсткая черта подо всем этим
Теперь, когда ждёт сервер, модель, сжигающая вызов инструмента на sleep 240, перестала быть хитрым обходным манёвром и стала чистым расточительством. Поэтому 0.10.4 добавил его в список злоупотреблений shell, рядом с watch и top:
Waiting with a bare `sleep` is forbidden — it burns the whole tool call
doing nothing.
To wait for a condition, poll it in a loop (sleep inside a loop body
is allowed):
until <check>; do sleep 2; done
To wait for a command you started, run it normally; long-running
commands automatically move to the background and notify you when
they finish.
Та же политика, что и с отказом от grep в 0.9.0: не намекать, а падать — и класть правильный ход в ошибку. Голый sleep octofs отклоняет; sleep внутри цикла until — легитимный опрос условия, и он проходит. watch и top он отклоняет, потому что они никогда не завершаются, а в событийном shell это значит: слот повышения они держали бы вечно, а уведомление о завершении не доставили бы никогда.
Нить под всем этим: меньше мест для галлюцинаций
Вокруг работы над shell пять релизов поменьше продолжали тянуть нить 0.9.0 — закрывать щели, в которых модель могла принять молчание или двусмысленность за информацию.
Пустой результат поиска говорит об этом вслух. Поиск, не нашедший ничего, мог бы и вернуть, собственно, ничего — а модель, получившая пустую строку, не всегда делает вывод «совпадений нет». Иногда она делает вывод «инструмент сломался» и повторяет попытку; иногда, что хуже, заполняет тишину тем, что ожидала найти, и действует так, будто нашла. Поэтому случай «нет совпадений» — это предложение, в котором сказано, что именно искали и что совпадений ноль, — поведение родом ещё из 0.7, которое 0.10.2 закрепил тестами, чтобы оно не могло тихо сломаться. Отсутствие свидетельств, сформулированное как свидетельство отсутствия.
Схемы инструментов избавились от вариантов null (0.10.3). «Опциональное как nullable» в JSON-схеме нормально читается человеком и служит соблазнительной ловушкой для модели: "path": null — это вызов, который не валидируется ни во что хорошее. Опциональное теперь значит отсутствующее.
Устаревшие идентификаторы строк сообщают о себе лучше (0.10.5). Ошибки верификации из 0.9.0 — те, что показывают свежее содержимое и куда переехала цель, — стали точнее и в том и в другом.
Листинг и поиск стали быстрее (0.10.1): подсчёт переводов строк по сырым байтам вместо конвертации в UTF-8 с потерями, типы файлов из обходчика каталогов вместо лишнего stat на каждую запись и префильтр по целому буферу, отбрасывающий несовпадающие файлы до построчной работы. Латентность инструмента, который модель вызывает сотни раз за сессию, — это налог на всё.
Удалённая машина без церемоний
Octofs говорит на SSH/SFTP с 0.8.0: наведите инструмент на ssh://user@host/path — и агент получает ту же проверяемую файловую систему на удалённой машине. Два релиза довели дело до конца.
0.14.0 разрешает цели через ~/.ssh/config. Алиасы Host, бастион ProxyJump, IdentityFile, IdentityAgent, пользователи и порты для конкретных хостов — конфигурация, которую вы когда-то написали для собственных пальцев, теперь действует и на подключения агента. Тест простой: если в вашем терминале работает обычный ssh box, то в octofs работает ssh://box/path — вместе с бастионом и всем остальным. (Честности ради — прыжок один: многозвенные цепочки ProxyJump и ProxyCommand мы отклоняем с внятной ошибкой, а не поддерживаем наполовину.) Раньше агенту требовалась полностью прописанная цель — та самая, от которой ваш конфиг и был призван вас избавить.
0.14.1 научил детектор злоупотреблений заглядывать внутрь ssh-команд. В истории с отказом от grep из 0.9.0 была дыра удалённой формы: ssh box 'grep -r TODO src/' проплывал мимо детектора, который уважал кавычки слишком вежливо, чтобы в них заглянуть. Теперь он разбирает удалённую команду сквозь опции и вложенность SSH и применяет те же правила: тот же view path="ssh://box/src" content="TODO", что заменяет локальный grep, заменяет и удалённый. Конвейеры по-прежнему разрешены, интерактивный SSH не тронут.
Клиентская сторона рукопожатия
Всё описанное выше — одна половина протокольного разговора. Вторая половина — то, что ваш агентский рантайм делает со ссылкой на ресурс и уведомлением resources/updated, и если он не делает ничего, фоновые задачи тихо деградируют обратно в опрос.
Наш агент Octomind строил свою половину в те же недели: у фоновых shell-задач настоящий жизненный цикл, они переживают уплотнение контекста (для этого и нужна ссылка на ресурс с командой в имени), а завершения попадают в сессию в момент прихода уведомления. Эта работа растянулась на цикл 0.47–0.48 и описана в посте о релизе Octomind 0.48.0, опубликованном на этой неделе, — вместе с остальным релизом, ушедшим в минус почти на десять тысяч строк продакшен-кода. Если хотите увидеть, как выглядит агент, которого shell перестал блокировать: он запускает сборку, правит следующий файл, пока сборка идёт, и читает вердикт, когда вердикт существует.
Впрочем, ничто в octofs не зависит от Octomind. Ресурсы задач, ссылки, уведомления — всё это обычный MCP: любой клиент, следующий протоколу, получает событийный shell бесплатно.
Обновление
# Homebrew
brew upgrade muvon/tap/octofs
# Cargo
cargo install octofs --version 0.14.1
# npm
npm install -g @muvon/octofs
Готовые сборки для Linux, macOS и Windows (x86_64 и ARM64) лежат на странице релизов.
Изменений в конфигурации не требуется. Одна заметка о поведении: если ваши промпты или клиентский код передавали инструменту shell флаг background, уберите его — флага больше нет, повышение автоматическое. Как и с переключателями режимов, удалёнными в 0.9.0, заменять его нечем: правильное поведение теперь единственное.
Разница проявится на первой же команде, которая переживёт десять секунд. Вместо заблокированной сессии, убитого вызова или запущенного дубля ваш агент получает обратно свой ход, ссылку на работающую задачу и уведомление, когда появится что читать.
Octofs — открытый исходный код (Apache 2.0), github.com/Muvon/octofs. Пост про 0.9.0 был о том, почему нельзя доверять номеру строки; этот — о том, почему нельзя доверять и заблокированному вызову инструмента. Принцип оба раза один: работа сервера — дать модели то, по чему она может действовать, а «подожди здесь, пока ничего не происходит» этим не было никогда.



