Octofs 0.14: esperar no es una llamada a herramienta
El agente lanzó la suite de tests. La suite tarda cuatro minutos. El timeout de inactividad del cliente MCP es de sesenta segundos.
Ya ves adónde va esto. En el segundo sesenta el cliente canceló la llamada. El proceso siguió ejecutándose —nadie le dijo que parara— mientras el modelo, con una cancelación entre las manos donde deberían estar sus resultados de tests, hizo lo razonable y volvió a lanzar la suite. Dos suites de tests, el mismo directorio, compitiendo entre sí por los mismos artefactos de compilación. La segunda falló con un error de bloqueo, el modelo informó de que los tests estaban rotos, y los tests estaban bien.
En otra sesión el mismo modelo, ya escarmentado, se inventó un apaño: lanzar la compilación, llamar a sleep 240 y después mirar. Una llamada a herramienta que no hace nada, mantenida abierta durante cuatro minutos, para que otra llamada distinta quizá tuviera algo que enseñar. El modelo había reinventado el polling, y mal, porque no le habíamos dado nada mejor.
La última vez que escribimos sobre octofs, el tema eran las ediciones que aterrizan en la línea equivocada. Aquel artículo terminaba con un principio: la interfaz real de un servidor MCP es cada cadena de texto que le devuelve al modelo. Las once versiones desde entonces —de la 0.10.1 a la 0.14.1, dos semanas— aplican el mismo principio a la cadena más lenta de todas: la que el modelo espera. El shell ahora es dirigido por eventos. Los comandos arrancan en primer plano, pasan solos a segundo plano si superan los diez segundos, y el cliente recibe una notificación cuando terminan. Nada bloquea, nada muere, nada se ejecuta dos veces.
Así llegamos hasta ahí, giro equivocado incluido.
Primer arreglo: demostrar que la llamada está viva
La cancelación a los sesenta segundos tenía una causa superficial y una profunda. La superficial: una llamada al shell es silenciosa por naturaleza. Una compilación en marcha no dice nada por el cable durante minutos, y para un cliente MCP el silencio es indistinguible de un servidor colgado. Así que 0.10.2 añadió latidos de vida: mientras un comando corre en primer plano, octofs emite una notificación de progreso cada diez segundos, muy por debajo de cualquier timeout de inactividad sensato, de modo que un solo latido perdido no pueda cancelar la llamada.
Eso paró las ejecuciones sumarias. No tocó el problema profundo: la llamada seguía bloqueando. Una suite de tests de cuatro minutos seguía costando cuatro minutos de sesión en los que el modelo no podía hacer nada: ni leer el archivo que fallaba, ni preparar la siguiente edición, ni pensar. Los latidos hacen que la espera sea sobrevivible. No la hacen útil.
Segundo arreglo: trabajos en segundo plano, y la opción que tuvimos que borrar
0.11.0 introdujo la ejecución en segundo plano: lanzas un comando como trabajo, recibes un identificador de inmediato y recoges la salida más tarde. Cada trabajo es un recurso MCP con un URI como octofs://jobs/17342-1, legible en cualquier momento para consultar su estado y su salida.
Salió con una opción background en la herramienta de shell, y esa opción fue un error que en retrospectiva reconocemos como uno ya conocido. En 0.9.0 borramos un interruptor --line-mode porque la seguridad que se entrega detrás de una opción es seguridad que casi nadie activa. La opción background era el mismo bug con otro traje: le pedía al modelo predecir la duración de un comando antes de ejecutarlo. Los modelos son malos en esto exactamente como cabría esperar —cargo build es instantáneo con la caché caliente y tarda seis minutos en frío— y la opción convertía ese dato incognoscible en una decisión obligatoria. Adivina segundo plano para un comando rápido y has añadido un viaje de ida y vuelta inútil. Adivina primer plano para uno lento y vuelves a la llamada bloqueada con la que empezamos.
Así que 0.13.0 borró la opción y sustituyó la predicción por una medición. Todo comando arranca en primer plano. Si a los diez segundos sigue corriendo, se promociona automáticamente a trabajo en segundo plano: el mismo proceso, ni matado ni reiniciado. La captura de salida es duradera desde el primer byte, así que cruzar el umbral no pierde nada: lo que el comando imprimió en su vida en primer plano está esperando en el log del trabajo cuando lo lees después.
La llamada a herramienta retorna de inmediato en el momento de la promoción, con un enlace de recurso que lleva el comando como nombre, de modo que un cliente puede mostrar «make test … sigue en ejecución» sin tener que reconstruir qué era el trabajo, incluso después de una compactación de contexto. Cuando el proceso termina, octofs emite notifications/resources/updated para el URI del trabajo. El cliente lee el recurso una vez y obtiene el código de salida y la cola de la salida. Sin polling, sin llamadas mantenidas abiertas, sin procesos huérfanos.
Dos detalles de ese flujo se ganaron su sitio por las malas:
- La cola, no la cabeza. Una lectura del recurso devuelve como máximo los últimos 30 KB de salida. Los logs de compilación son largos, y el veredicto —el error, el resumen final de los tests— vive al final. Darle a un modelo los primeros 30 KB de un log cuya última línea dice
FAILEDes la receta para un informe muy seguro de que todo pasó. - Dos vías de entrega. Los clientes en la revisión 2026-07-28 de MCP que abrieron un flujo de suscripción reciben la finalización por él; los clientes más antiguos reciben el push no solicitado que permitía la especificación anterior. Y desde 0.13.0, a un cliente que se suscribe tarde —con el trabajo ya terminado— se le reproduce la finalización en lugar de dejarlo esperando para siempre una notificación que se disparó antes de que nadie estuviera escuchando.
La ventana de primer plano también se redujo de treinta segundos a diez en 0.13.0, y eso es la promoción automática pagándose sola: cuando cruzar la frontera no cuesta nada —el mismo proceso, salida duradera, una notificación al final— no hay razón para tener la sesión secuestrada medio minuto por si acaso el comando termina en el segundo veinticinco.
Tercer arreglo: dejar que los trabajos corran unos junto a otros
0.11.0 era conservadora: un trabajo por directorio, y punto. Segura, y demasiado tosca: serializaba una compilación y un tail de logs que no tenían por qué esperarse el uno al otro.
0.14.0 estrechó la guarda al único caso que de verdad es un bug: el comando idéntico que ya está corriendo en el mismo directorio. Eso no es concurrencia, eso es la suite de tests disparada dos veces de la historia del principio, y en lugar de dejarla competir octofs la rechaza y le dice al modelo exactamente qué hacer:
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.
Los comandos distintos corren en paralelo. El duplicado recibe un error que es, una vez más, la instrucción de recuperación.
Y una línea dura debajo de todo esto
Con el servidor encargándose de esperar, que un modelo queme una llamada a herramienta en un sleep 240 dejó de ser un apaño ingenioso y pasó a ser puro desperdicio. Así que 0.10.4 lo añadió a la lista de mal uso del shell, junto a watch y 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.
La misma política que el rechazo de grep en 0.9.0: no insinúes, falla, y pon la jugada correcta dentro del error. Un sleep pelado, octofs lo bloquea; un sleep dentro de un bucle until es un sondeo legítimo de una condición y pasa. A watch y top los bloquea porque nunca terminan, lo que en un shell dirigido por eventos significa que ocuparían para siempre un hueco de promoción y no entregarían nunca la notificación de finalización.
El hilo de fondo: menos sitios donde alucinar
Alrededor del trabajo en el shell, cinco versiones menores siguieron tirando del hilo de 0.9.0: cerrar los huecos donde un modelo podía confundir el silencio o la ambigüedad con información.
Los resultados de búsqueda vacíos lo dicen en voz alta. Una búsqueda sin resultados podría devolver, pues eso, nada — y un modelo al que le entregas una cadena vacía no concluye de forma fiable «sin coincidencias». A veces concluye «la herramienta falló» y reintenta; a veces, peor, rellena el silencio con lo que esperaba encontrar y sigue adelante como si lo hubiera encontrado. Así que el caso sin coincidencias es una frase que dice qué se buscó y que existen cero coincidencias — un comportamiento que se remonta a 0.7, y que 0.10.2 fijó con tests para que no pueda degradarse en silencio. La ausencia de evidencia, enunciada como evidencia de ausencia.
Los esquemas de herramientas perdieron sus variantes null (0.10.3). Lo opcional-como-anulable en un esquema JSON se lee bien a ojos humanos y para un modelo es un señuelo peligroso: "path": null es una llamada que valida y no lleva a ningún sitio bueno. Opcional ahora significa ausente.
Los identificadores de línea obsoletos informan mejor (0.10.5). Los errores de verificación de 0.9.0 —los que muestran el contenido fresco y adónde se movió tu destino— se volvieron más precisos en ambas cosas.
Listar y buscar se volvieron más rápidos (0.10.1): conteo de saltos de línea sobre bytes crudos en lugar de una conversión UTF-8 con pérdidas, tipos de archivo tomados del recorredor de directorios en lugar de un stat redundante por entrada, y un prefiltro de búfer completo que descarta los archivos sin coincidencias antes del trabajo a nivel de línea. La latencia en una herramienta que el modelo llama cientos de veces por sesión es un impuesto sobre todo lo demás.
La máquina remota, sin ceremonias
Octofs habla SSH/SFTP desde 0.8.0: apunta una herramienta a ssh://user@host/path y el agente obtiene el mismo sistema de archivos verificado en una máquina remota. Dos versiones remataron el trabajo.
0.14.0 resuelve los destinos a través de ~/.ssh/config. Alias de Host, un bastión ProxyJump, IdentityFile, IdentityAgent, usuarios y puertos por host: la configuración que ya escribiste para tus propios dedos ahora se aplica a las conexiones del agente. La prueba es simple: si un ssh box a secas funciona en tu terminal, ssh://box/path funciona en octofs, bastión incluido. (Un solo salto, para ser honestos: las cadenas ProxyJump de varios saltos y ProxyCommand las rechazamos con un error claro en lugar de soportarlas a medias.) Antes de esto, el agente necesitaba el destino escrito con todas sus letras, ese que tu archivo de configuración existe para ahorrarte.
0.14.1 hizo que la detección de mal uso lea dentro de los comandos ssh. La historia del rechazo de grep de 0.9.0 tenía un agujero con forma de remoto: ssh box 'grep -r TODO src/' pasaba de largo ante un detector que respetaba las comillas con demasiada educación para mirar dentro de ellas. Ahora analiza el comando remoto a través de las opciones y el anidamiento de SSH y aplica las mismas reglas: el mismo view path="ssh://box/src" content="TODO" que sustituye al grep local sustituye al remoto. Las tuberías siguen permitidas, el SSH interactivo sigue intacto.
El lado del cliente en el apretón de manos
Todo lo anterior es una mitad de una conversación de protocolo. La otra mitad es lo que tu runtime de agente haga con un enlace de recurso y una notificación resources/updated — y si no hace nada, los trabajos en segundo plano degeneran silenciosamente de vuelta al polling.
Nuestro agente Octomind construyó su mitad durante esas mismas semanas: los trabajos de shell en segundo plano se rastrean con un ciclo de vida real, sobreviven a la compactación de contexto (eso es el enlace de recurso llevando el nombre del comando) y las finalizaciones aterrizan en la sesión en el momento en que llega la notificación. Ese trabajo abarcó el ciclo 0.47–0.48 y se cuenta en el artículo de lanzamiento de Octomind 0.48.0, publicado esta semana — junto con el resto de una versión que quedó en negativo neto por casi diez mil líneas de código de producción. Si quieres ver cómo es un agente cuando el shell deja de bloquearlo: lanza la compilación, edita el siguiente archivo mientras la compilación corre y lee el veredicto cuando el veredicto existe.
Eso sí, nada en octofs depende de Octomind. Los recursos de trabajo, los enlaces, las notificaciones son MCP puro: cualquier cliente que siga el protocolo obtiene el shell dirigido por eventos gratis.
Actualizar
# Homebrew
brew upgrade muvon/tap/octofs
# Cargo
cargo install octofs --version 0.14.1
# npm
npm install -g @muvon/octofs
Los binarios precompilados para Linux, macOS y Windows (x86_64 y ARM64) están en la página de releases.
No hace falta ningún cambio de configuración. Una nota de comportamiento: si tus prompts o el código de tu cliente pasaban una opción background a la herramienta de shell, quítala — la opción ya no existe y la promoción es automática. Como con los interruptores de modo que borramos en 0.9.0, no hay nada con qué reemplazarla; el comportamiento correcto es ahora el único comportamiento.
La diferencia aparece en el primer comando que supere los diez segundos. En lugar de una sesión bloqueada, una llamada matada o una ejecución duplicada, tu agente recupera su turno, recibe un enlace al trabajo en marcha y una notificación cuando hay algo que valga la pena leer.
Octofs es código abierto (Apache 2.0) en github.com/Muvon/octofs. El artículo de 0.9.0 contaba por qué no se puede confiar en un número de línea; este contaba por qué tampoco se puede confiar en una llamada a herramienta bloqueada. El mismo principio las dos veces: el trabajo del servidor es entregarle al modelo algo sobre lo que pueda actuar, y «espera aquí mientras no pasa nada» nunca lo fue.



