Skip to content

Recuperación NFS y hold de mantenimiento — 20 octubre 2026

Continuación de las comprobaciones de capacidad y snapshots. Trabajo en checkout separado; Vera y sus commits siguen bajo el control de Ramon.

Corrección del arranque del lock, 22 octubre 16:30–16:32 CEST

El monitor registraba NFS sano en sus35 últimas líneas, pero salía75 con NFS_RECOVERY_UNKNOWN. Faltaba /tmp/homelab-ctl.lock tras el reboot: las tareas watch/update de RunWisp están retenidas y no crean el archivo. El perfil completo sigue siendo el HELD conocido; no se cambió ni exportó. El pendiente se conserva.

Se instaló /etc/tmpfiles.d/homelab-ctl.conf, root0644, con esta regla nativa:

f /tmp/homelab-ctl.lock 0600 1000 1000 - -

Es el mismo pathname usado por ambos flock de RunWisp en media208 y por la guarda NFS. El lock de /run/lock del controlador central CT212 es distinto. La operación f crea si falta, conserva un inode existente y puede ajustar permisos/propiedad; no es f+ ni una sustitución de locks. Véase el manual de systemd249. La creación nativa y una segunda aplicación conservaron el inode abierto; owner1000:1000 y modo0600 comprobados. No se encontró un descriptor abierto con el pathname antiguo o eliminado antes de crear el archivo ausente.

A las16:32 el siguiente ciclo ordinario del monitor devolvió NFS_RECOVERY_HELD, conservando75 y el pendiente. Esto corrige la inicialización y distingue la retención intencional del error UNKNOWN anterior; no habilita recuperación, no limpia la alerta ni acredita reparar NFS. No se ejecutó el monitor, una copia, un rebind o una notificación como prueba. Un primer intento se detuvo antes de escribir por fijar un boot histórico; se conservó su registro.

Fuente en ansible/roles/homelab_tools/{tasks/lifecycle_lock.yml,files/homelab-ctl-tmpfiles.conf}, limitada a media208. El rol conserva never; no se espera convergencia ordinaria. Para reconstruir sólo esta configuración, revisar primero el estado actual y usar:

ansible-playbook -i ansible/inventory.yml ansible/playbook.yml --limit media-208 --tags homelab_tools_lifecycle_lock --skip-tags always

La selección --list-tasks se verificó localmente; la instalación real fue selectiva sobre ese archivo y el lock, sin ejecutar el playbook completo ni tocar el TOML privado. El archivo queda disponible para el tmpfiles de arranque; no se provocó un reboot para demostrar esa fase. Recibos privados: nfs-health-failure-20261022/{guard-metadata.json,apply-lock-v2.json,after-ordinary.json}.

Problema y comportamiento preparado

Retener watch/update en RunWisp no detenía el monitor NFS. Sus rutas de recuperación todavía podían ejecutar Compose, reiniciar Docker/autofs o arrancar contenedores. Eso también podía aplicar el checkout de Compose durante el hold.

La recuperación y el rebind pasan ahora por una guarda común. La reconciliación sólo solicita permiso si existe un pendiente. La guarda conserva el mismo flock existente del actualizador durante toda la operación y las llamadas anidadas. Cada punto de mutación vuelve a verificar el descriptor y el perfil activo. Un permiso desconocido, un archivo inseguro, un lock ocupado o un hold activo impiden la reparación y devuelven75; no se crean ni sustituyen locks.

La comprobación sana sin pendientes sigue recogiendo estado. Las lecturas y snapshots no autorizan por sí mismas ninguna reparación. Si la reparación queda bloqueada o parcial se conserva el pendiente; el chequeo no reemplaza entonces el baseline. Un rebind fallido ya no genera el mensaje de recuperación completa. Una inspección fallida o inválida tampoco se interpreta como contenedor parado.

Perfil y límite de disponibilidad

VM208 tiene Python3.10.12; no se presupone tomllib ni se instala un parser. La guarda usa perfiles completos revisados de RunWisp, mediante SHA256 del archivo privado completo; no imprime el documento ni valores de credenciales.

  • Unheld revisado: <REDACTED>.
  • Held conocido: <REDACTED>.
  • Cualquier otro contenido, fragmentos adicionales o metadatos inesperados: desconocido, sin reparación automática.

Un cambio legítimo futuro del TOML también puede bloquear la recuperación NFS. Debe revisarse el perfil completo y actualizarse explícitamente la guarda; no basta con copiar un hash nuevo ni eliminar su comprobación. Las tareas de backup y los horarios de RunWisp no se modifican por esta guarda.

Mientras el hold impida reparar, una avería NFS necesita intervención en una ventana revisada: la barrera sacrifica recuperación automática para impedir reinicios o despliegues durante el diagnóstico de estabilidad. No se presenta esa situación como disponibilidad normal.

Fuente y runtime, antes de instalar

Lectura pasiva de metadatos alrededor de08:54 CEST:

  • Servicio nfs-health-monitor inactivo, último resultado success/0, User root.
  • Script regular1000:1000/0755, hash <REDACTED>.
  • El runtime coincide exactamente con la fuente anterior a c7eccd2 salvo la asignación NTFY_URL. Esa diferencia se conserva en la copia privada y deberá preservarse en cualquier candidato de instalación; no se publica su valor.

Copia privada local de revisión: ~/.local/state/nfs-recovery-review-20261020/runtime-before.sh, modo0600. Esta observación no acredita salud NFS, un montaje remoto real ni ausencia de una ejecución posterior. No se ha ejecutado el monitor completo, ningún recover/rebind ni una notificación como prueba.

Trabajo que sigue pendiente

La guarda no convierte toda la recuperación en segura: cuando el perfil permite actuar, la selección histórica aún usa nombres, hay rebind sobre todos los binds y Compose puede arrancar servicios dormidos. Para resolverlo hacen falta IDs inmutables y un diario de autorizaciones preavería, con coordinación Sablier; una lectura running seguida de restart no elimina esa carrera.

También quedan pendientes la prueba de montaje NFS real, la promoción atómica de pendientes y un presupuesto global de tiempo. No liberar el hold para probar esas rutas ni declarar la recuperación completa por tener esta guarda.

Validación de la fuente

Revisión independiente de Bash y del módulo Python correcta.35 casos NFS (13 snapshots,8 flujos de reparación y14 de guarda) pasan con dobles locales. Incluyen un hijo Bash benigno real que verifica la herencia del descriptor y una llamada anidada, sin ejecutar el monitor de producción. Los ocho puntos de mutación tienen checkpoint previo; se prueban bloqueo, fallo de inspección, conservación de pendientes/contador y ausencia de recuperación verde falsa. Junto a15 casos N‑1 y6 balloon son56 pruebas de la CI específica para Python3.10 y3.12. No se ejecutan Docker, SSH, NFS ni notificaciones reales.

La guarda no tiene deadline global ni limpieza de grupos de procesos: tras cancelar el padre puede quedar un hijo vivo con el lock. Un timeout de observación no acredita que la reparación haya terminado; comprobar el proceso antes de actuar de nuevo, sin borrar el lock para forzar otra operación.

Estado al preparar esta publicación: fuente validada, instalación pendiente. El candidato de instalación debe preservar NTFY_URL, ambos archivos y sus permisos; bajo un perfil held conocido, publicar primero el Bash protegido deja las reparaciones bloqueadas incluso si aún falta el módulo. Dos renames no constituyen una transacción: un fallo parcial requiere lectura de hashes y recibo antes de intervenir, sin restaurar automáticamente el Bash sin guarda.

Convergencia natural de normalidad, alrededor de09:04 CEST

Una lectura mínima de cada host confirmó el helper N‑1 publicado previamente, hash <REDACTED>:

  • Pmx50: pull08:58:30–08:59:16, success, status0, inactivo y MainPID0.
  • Pmx51: pull08:59:59–09:01:23, success, status0, inactivo y MainPID0.

No se forzó el pull ni se ejecutó normalidad completo. Esta convergencia acredita la versión instalada, no el margen N‑1 actual ni compatibilidad funcional con toda salida futura de HA. La guarda NFS publicada en0f55754 tiene CI39384104208 correcta:15+6+35 casos en cada Python3.10 y3.12.

Instalación selectiva y comprobación nativa,09:08–09:09 CEST

Instalación finalizada alrededor de09:08:11 CEST, sin ejecutar recover/rebind, notificaciones ni jobs, y sin parar o reprogramar timers. El instalador comprobó el perfil held completo e inalterado, el flock existente, servicio inactivo, MainPID0 y cero procesos monitor/guarda; conservó la URL local de avisos.

  • Monitor1000:1000/0755: <REDACTED>.
  • Helper nuevo1000:1000/0644: <REDACTED>.
  • Backup privado y recibo: /root/rollback-nfs-guard-2xv1390g/ en VM208; before.sh conserva el monitor anterior, receipt.json acredita ambos renames.
  • Instalador local revisado: ~/.local/state/nfs-recovery-review-20261020/install_guard.py, hash <REDACTED>. Cinco fixtures adicionales extraen exactamente su clasificador de procesos por AST y pasan sin SSH. Compilación Python y sintaxis Bash nativas correctas.

A09:09:17 CEST se cargó el helper instalado con nombre de módulo de validación (sin main), se abrió el lock existente y se llamó exclusivamente a verify. Resultado HELD bajo Python3.10.12 y configuración real, sin invocar ninguna operación de recuperación. La lectura de metadatos de09:08:25 encontró timer activo y último ciclo09:08:00–09:08:04 success/0: ese ciclo fue anterior a la instalación; no se usa como prueba del nuevo monitor.

Para recuperar, leer primero hashes y recibo. Restaurar automáticamente el Bash anterior reabriría el bypass del hold: antes hay que revisar la ventana, la barrera y el estado de las operaciones. No eliminar ni reemplazar el lock. El proceso de instalación no ofrece exclusión absoluta frente a una ejecución manual o un timer antiguo que no respeten ese lock entre comprobación y rename; las comprobaciones de inactividad reducen esa ventana, sin declararla imposible.

Primer ciclo natural posterior confirmado

Lectura pasiva a09:10:56,513685 CEST: ciclo09:10:00–09:10:04, success, ExecMainStatus0, MainPID0 e inactivo. El ExecStart del servicio contiene la ruta del monitor instalado. Timer activo, siguiente ejecución09:12:00 CEST. Este ciclo sí es posterior al recibo de instalación09:08:11. No se forzó ningún job. Acredita ejecución del monitor después del reemplazo; no es un ensayo de recuperación frente a una avería, ni demuestra un montaje NFS real por sí solo.

Seguimiento: montaje efectivo y pendientes íntegros

Fuente preparada después de09:13 CEST, con revisión independiente. El chequeo ya no acredita NFS por encontrar un directorio accesible: el helper readonly mount usa el FD de ubicación y su mnt_id para seleccionar el registro del montaje efectivo. Exige el target exacto, raíz /, tipo nfs/nfs4 y export esperado; una segunda apertura fresca debe conservar el mismo ID. No elige la primera línea ni el mayor ID. Se aceptan capas autofs+NFS cuando el FD señala NFS; se rechaza NFS oculto por una capa local. Remount exige la comprobación completa.

Fuentes primarias: open(2), O_PATH y proc_pid_fdinfo(5), mnt_id. El descriptor no lee contenidos; los accesos ls/stat previos del monitor se conservan. La muestra no congela el namespace ni garantiza integridad de datos. Una fixture privada en VM208 encontró util-linux2.37.2 y varias filas para un stack sintético con findmnt; no se asumió el comportamiento de una versión nueva. No se crearon montajes ni se contactó el NAS en esa fixture.

Lecturas exclusivas de /proc/self/mountinfo de VM208 alrededor de09:13 y09:18 confirmaron registro del target /mnt/nfs_media, raíz /, nfs y source esperado. Esas lecturas se usaron para comprobar la configuración, no como prueba de accesibilidad ni de visibilidad efectiva bajo carreras.

La promoción de pendientes copia completamente el baseline a un temporal0600 del mismo directorio y lo publica mediante GNU ln-T sin sobrescritura. Un pendiente regular existente, incluido uno vacío, conserva sus bytes e inode; una colisión durante publicación tampoco lo sustituye. Se rechazan enlaces, archivos ilegibles y baseline ausente cuando no hay un pendiente anterior. El estado final como symlink se rechaza antes de aceptar pendientes existentes. La recuperación no avanza si falla esa promoción. La reconciliación exige lectura completa y retirada comprobada antes de anunciar finalización.

Validación preparada:75 pruebas NFS, más15 N‑1 y6 balloon (96 en total). Mac ejecutó74 de las75 NFS; sólo omite la fixture nativa O_PATH, que se ejecuta en Linux. El adaptador de ln usa os.link en Mac y GNU /bin/ln -T real en CI Linux. La revisión de ambas partes no encontró bloqueadores tras el ajuste del symlink de STATE_DIR. La validación Linux y la instalación de este seguimiento se deben registrar aparte: todavía no se acreditan por esta preparación.

Límites conservados: ancestros y escritores privilegiados del estado son de confianza; publicación atómica no equivale a durabilidad fsync frente a caída del host. No se ha implementado todavía el diario por ID ni coordinación Sablier, y no se libera el hold.

Aplicación del seguimiento y corrección de compatibilidad nsfs

Fuente f979feb, CI39386347385:96 casos correctos en cada Python3.10/3.12, incluidos O_PATH y GNU ln-T nativos. La primera instalación se detuvo antes de modificar archivos, coincidiendo con el ciclo09:26. Se releyeron hashes, perfil y servicio inactivo antes de una nueva ejecución, sin reintento ciego.

El segundo intento terminó09:26:37 CEST, con helper compatible primero y Bash después, preservando NTFY local y1000:1000/0755+0644. Backup de ambos originales y recibo: /root/rollback-nfs-mount-pending-flbtwhx1/. Monitor instalado: <REDACTED>. Instalador privado revisado install_mount_pending.py, hash <REDACTED>. No se invocaron recover/rebind, notificaciones ni cambios de timers.

La prueba nativa detectó un defecto no cubierto por aquella CI: el parser rechazaba raíces opacas net:[…] de montajes nsfs ajenos al NFS. El FD seleccionado sí correspondía al target, raíz, tipo y export esperados. Se corrigió para mantener la exigencia de raíz absoluta sólo sobre el registro seleccionado, sin relajar expected_mount ni el hold. Dos regresiones nuevas cubren ese caso; fuente5a28045, CI39386930115:98 casos correctos en cada Python3.10/3.12, sin skips.

Helper corregido instalado09:30:13, hash <REDACTED>. Antes del reemplazo, la versión candidata acreditó EXPECTED_NFS en el namespace real y HELD con el lock/config reales. Backup del helper previo y recibo: /root/rollback-nfs-nsfs-t58d7dff/.

Los ciclos09:28 y09:30 crearon dos fallos de prueba y un pendiente falso; el hold bloqueó la reparación. Se comprobó que las dos incidencias eran de la prueba de montaje, sin timeout ls/stat ni mountpoint ausente; el pendiente coincidía byte a byte con el baseline y sus19 contenedores seguían running. Después de verificar EXPECTED_NFS+HELD y comprobar identidad/metadatos, se archivaron reversiblemente el pendiente y el contador09:31:52 en /root/rollback-nfs-false-proof-_wnjo2i9/, con recibo. No se tocó el baseline ni se arrancó o reinició ningún contenedor.

Estado observado al cerrar este seguimiento

La prueba de montaje efectiva corregida pasa en VM208; el hold sigue activo. El ciclo09:32 obtuvo NFS OK pero no pudo completar snapshot: conservó el baseline. Una fixture de diagnóstico con las mismas dos funciones reales, lecturas Docker y un STATE_DIR privado terminó correctamente con19 running, sin escribir el estado real; evidencia privada /root/nfs-snapshot-diagnostic-pk_pznqj/. La colección directa de metadatos también mostró90 contenedores,24 con binds NFS y ninguna inspección fallida o estado inválido en esa lectura posterior. Eso no explica retrospectivamente el fallo del ciclo.

El baseline real se actualizó naturalmente09:34:04 con19 registros. El ciclo 09:36 volvió a terminar con1 y conservó ese baseline; no había pendiente ni contador de fallo NFS, y el timer permanecía activo. Queda pendiente diagnosticar este fallo intermitente de snapshot, sin atribuirlo a Docker, permisos o I/O sin evidencia. No se declara todo el monitor resuelto por la CI o por la prueba de montaje. Continúan pendientes IDs/Sablier y el presupuesto global de tiempo.

Inventario por ID y reintento completo (09:49 CEST)

La comparación acotada de los ciclos09:32/09:34/09:36 encontró una diferencia útil: los dos fallidos consultaron91 montajes y sólo llegaron a una inspección Running; el correcto consultó90 montajes y24 estados. El primer nombre de los dos ciclos fallidos era el mismo y ya no existía en la lectura posterior. Esto apoya una carrera por desaparición entre consultas; como el stderr histórico se descartó, no demuestra retrospectivamente una causa única.

El subcomando readonly snapshot sustituye esa colección por ps -aq --no-trunc y un único inspect por IDs completos, con proyección JSONL limitada a ID, nombre, Running y orígenes de montajes. Valida todas las filas y exige el conjunto exacto de IDs, claves únicas, nombres válidos y booleanos estrictos antes de emitir resultado. Una colección fallida se descarta por completo y se permite un único reintento desde un inventario nuevo. Un error persistente no publica stdout ni sustituye el baseline. Inventario vacío correcto no invoca inspect sin argumentos. Se conserva la selección Running=true con origen NFS exacto o subruta, excluyendo colisiones de prefijos.

Se fija el socket Docker local y se retiran cinco variables de control Docker sólo del entorno de los subprocesos. Root consulta Docker directamente; monxas usa sudo-n; otros usuarios fallan de forma conservadora. El caso normal requiere dos CLI, el vacío uno y un reintento completo como máximo cuatro. Cada CLI tiene timeout10s y se aceptan hasta256 IDs y2MiB por salida capturada; este último límite es de aceptación después de capturar, no un límite duro de memoria. No es una fotografía atómica del estado Docker. El baseline sigue guardando nombres y no resuelve el diario futuro por ID ni Sablier.

Revisión independiente sin bloqueadores. Pruebas:91 NFS (90 ejecutadas en Mac, una nativa Linux pendiente en CI), más15 N-1 y6 balloon:112 casos en total. La candidata helper se ejecutó sólo en memoria en VM208, como root, sin instalar ni invocar el monitor: devolvió19 nombres únicos en0,128s mediante la proyección Docker real. No escribió estado ni cambió contenedores. Esto verifica el ABI nativo de esa consulta; no acredita todavía instalación ni ciclos naturales con la candidata.

Instalación del inventario y primer ciclo natural (09:52 CEST)

Fuent publicada en main. CI39389118981:112 pruebas por cada Python3.10/3.12, sin omisiones, incluidas las nativas O_PATH/GNU ln-T. Docs39389119020 también terminó correctamente.

La lectura09:50:24 confirmó hashes anteriores51de6aa8/40cb4998 y perfil heldf7b3a3ad. El servicio estaba failed/failed con MainPID0 tras el ciclo09:50, que conservó el baseline09:46. El instalador revisado acepta sólo inactive/dead/0 o failed/failed/0 y exige además cero procesos del monitor/helper; no ejecuta reset-failed ni altera el timer. Tres pruebas de la función idle exacta y cinco del clasificador /proc exacto pasaron. Instalador privado install_snapshot_collection.py, SHA <REDACTED>.

Instalación terminada09:51:42, primero helper compatible y después monitor, con CAS de pareja, bloqueo compartido, configuración exacta held, identidad y metadatos comprobados antes de cada reemplazo. Se preservó NTFY local sin publicar su valor. Backup de ambos archivos y recibo: /root/rollback-nfs-snapshot-collection-b472997z/. Monitor runtime: <REDACTED>. Helper runtime: <REDACTED>. Propietario1000:1000 y modos0755/0644 conservados. No se invocó reparación, reinicio de aplicaciones, notificación sintética ni ejecución manual del monitor. Los dos reemplazos no constituyen una transacción atómica frente a escritores externos; no hubo rollback ni reintento automático de instalación.

A09:52:01 el helper instalado confirmó EXPECTED_NFS y HELD usando el namespace, config y lock reales. El ciclo natural09:52 empezó y terminó dentro de ese segundo, Resultsuccess/ExecMainStatus0. El baseline se renovó09:52:00.573240 con 19 registros/208bytes; no existían pending-restore ni contador de fallos. La lectura de su journal contó un NFS OK y cero errores de snapshot. El timer seguía active/waiting con próxima ejecución09:54. Es el primer ciclo posterior correcto, no una garantía de ausencia futura de carreras o fallos. Continúan pendientes seguimiento sostenido, diario de recuperación por ID/Sablier y presupuesto global de recuperación.

Fuente pendiente: plazo de notificación

En una corrección posterior sólo de fuente, notify() añade a curl --connect-timeout 5 --max-time 20. El envío sigue siendo best-effort: un fallo no cambia el resultado de recuperación y no provoca reintentos. No se envió ninguna notificación de prueba ni se instaló este cambio.

Estos límites acotan cada envío de notificación; no constituyen un presupuesto global de recuperación ni resuelven los posibles bloqueos de NFS o del kernel. La suite local descubre91 pruebas NFS:90 pasan y una se omite en macOS porque requiere Linux/O_PATH. La sintaxis Bash y la revisión del diff pasan.

Notificaciones acotadas instaladas y seguimiento (10:00 CEST)

Fuente78fceeb publicada en main. CI39390007368 pasó112 casos porPython3.10/3.12; documentación39390007450 correcta. Instalador privado de un solo reemplazo install_notify_bound.py, SHA <REDACTED>, revisado por dos agentes y con8 pruebas exactas de idle/proc correctas. Instalación09:59:45: sólo Bash, preservando NTFY y0755/1000:1000; helper32589781 mantuvo bytes, inode y metadatos completos. Perfil held exacto, lock, pareja de hashes, ausencia de proceso y servicio detenido comprobados. Backup y recibo: /root/rollback-nfs-notify-bound-k2g16cz5/. Monitor runtime: <REDACTED>.

A09:59:07 el journal confirmó NFS OK sin errores de snapshot en los cuatro ciclos09:52/09:54/09:56/09:58. A10:00:09 se confirmó EXPECTED_NFS+HELD y el primer ciclo natural posterior al límite de notificación:10:00:00, RC0, baseline20registros actualizado10:00:00.766134, sin pendientes ni contador. La variación19/20 de registros es una observación del inventario, no una lista fija ni prueba de avería. No se enviaron avisos de prueba ni se ejecutó reparación. El ciclo sano no ejercita el timeout HTTP; acredita continuidad del monitor.

Siguiente reparación: identidad y grupos bajo demanda

La revisión de fuentes detecta que repair_media_services todavía usa una lista fija y puede escalar a reinicio global Docker; rebind_nfs_containers_impl selecciona ps-a y podría arrancar contenedores dormidos. El hold instalado impide esas reparaciones mientras permanece aplicado. No se considera cerrada la política de recuperación por haber corregido su inventario.

Sablier tiene proveedor Docker mediante su proxy sin montar el lock de VM208 (homelab-stacks/sablier/compose.yaml). La allowlist services/control-plane-access/sablier-socket.conf permite start/stop/wait sin esa barrera. El enrutador deCT212 usa otro lock /run/lock/homelab-ctl.lock, frente a /tmp/homelab-ctl.lock deVM208. No hay cooperación demostrada entre ellos.

La clasificación debe proteger también todos los integrantes de tunnel.lazy.names y rutas estáticas lazy, no sólo el contenedor etiquetado; Jellyfin Share incluye su DB. Contratos on-demand y sus dependencias también son protección conservadora. Una foto running antigua no autoriza despertar un grupo Sablier cuyo TTL haya caducado. Se prepara un manifiesto/plan por ID completo con estado manual explícito para identidades cambiadas, grupos bajo demanda y clasificación desconocida. Esa preparación no está integrada ni instalada y todavía requiere contrastar la política con las fuentes vivas.

Planificador por IDs preparado en fuente (10:03 CEST)

Nuevo módulo puro scripts/vm208/nfs-recovery-plan.py, sin integración con el monitor ni instalación. make_manifest(snapshot,now) conserva ID completo, nombre y conjunto canónico de orígenes NFS de contenedores running y no paused. plan_recovery(prior,current,policy,now) sólo propone restart/start por el ID anterior cuando coinciden identidad, nombre y orígenes NFS y la clasificación explícita es always sin protección. IDs ausentes/reutilizados, renombres, montajes cambiados, paused y grupos protegidos/desconocidos quedan manuales. Contenedores nuevos no ganan permiso por aparecer en el inventario actual.

El CLI recibe JSON acotado a2MiB por stdin, rechaza claves duplicadas y esquemas incoherentes, y sólo escribe JSON. No descubre archivos, llama Docker, adquiere locks ni ejecuta acciones. Su restored siempre es false; plan_complete sólo indica ausencia de filas manuales. Un plan mixto contiene propuestas y pendientes, nunca una recuperación exitosa. La política contiene afirmaciones explícitas del caller, no prueba de contratos vivos ni del cierre de grupos Sablier. Validar cronología UTC no concede autorización por edad del snapshot.

La revisión encontró una incompatibilidad antes de publicar: Source vacío de un montaje tmpfs ajeno a NFS se rechazaba. Se permite como noNFS, conservando validación de rutas no vacías, límites, NUL y componentes relativos en NFS. Diecisiete pruebas nuevas cubren identidades, fuentes, paused, clasificación, JSON, límites y ausencia de procesos; revisión independiente sin bloqueadores. La suite NFS pasa108 casos descubiertos en Mac (107 ejecutados, uno O_PATH nativo omitido). La CI incluye el nuevo módulo en sus disparadores push/PR.

Pendiente para integrar: el colector actual no proyecta paused; no se debe inventar false. Faltan el adaptador de inventario y evidencia de política viva, la migración explícita del baseline por nombres y un ejecutor con comprobaciones frescas de hold/montaje/identidad y tratamiento de carreras. El nuevo módulo no modifica ni sustituye todavía el camino de reparación instalado.

Adaptador de inventario preparado y consulta nativa (10:07 CEST)

El helper fuente añade el subcomando readonly inventory para producir el snapshot versionado del planificador: IDs completos, nombre normalizado, running, paused obligatorio y orígenes de montajes. La fecha UTC se asigna al terminar la colección completa. Un inventario vacío válido produce JSON; una colección fallida no produce datos parciales. Comparte el único colector con el snapshot anterior, incluidos socket local fijo, UID, límites y reintento. Se conserva literalmente el formato anterior de cuatro campos y su salida por nombres; funciones de lock/perfil/run/verify/mount idénticas por AST.

Doce pruebas nuevas, incluida la cadena inventario→manifiesto→plan y pausas sin valor implícito. Revisión independiente sin bloqueadores; suite NFS120 casos (Mac119 ejecutados, uno O_PATH nativo omitido). El helper candidato y el planner se ejecutaron sólo en memoria en VM20810:06:50:90 contenedores, ninguno paused, manifiesto de19 NFS running/no-paused,0,136s. No se instalaron, persistieron manifiestos ni ejecutaron recuperaciones. La consulta no es atómica y no acredita clasificación de ciclo de vida.

Lecturas separadas de metadatos10:06:16/39 hallaron24 contenedores NFS,19 running, y11 contratos cuyos campos seleccionados coinciden con Git. Diez contratos locales concuerdan con servicio y archivo Compose de sus etiquetas Docker; Audiobookshelf tiene execution_host personal-218. No se aplica ese contrato al homónimo antiguo deVM208. Candidatos primarios always por contrato/propietario: immich-server,plundrio,plundrio-onepacerr,plundrio-scanner,radarr,sonarr,trip-planner. No son una autorización de reinicio ni una política final; etiquetas/grupos bajo demanda y rutas activas pueden protegerlos y los servicios sin cobertura quedan sin clasificar. No se infiere always por ausencia de etiqueta ni para dependencias.

A10:07 ambos archivos managed.caddy de270/271 eran idénticos, SHA , con9 grupos Sablier/17 integrantes. Esa igualdad es sólo de archivos: no prueba la configuración cargada ni la cobertura de rutas estáticas. El parser inicial de static no completó y la copia de trabajo deVM208 no se pudo leer mediante el comando forzado del controlador. Se investigan esas limitaciones con lecturas acotadas; no se marca lazy_groups_complete ni se integra el ejecutor por ello.

Política contrastada y adaptador instalado (10:15 CEST)

Fuente2a382a0 publicada; CI39391293640 pasó141 casos porPython3.10/3.12, sin omisiones. Docs39391293588 correcta. La revisión independiente del adaptador confirmó ABI anterior y funciones de guardia sin cambios.

El contraste de rutas quedó resuelto por lecturas de la API local de configuración Caddy, sin petición a aplicaciones. Ambos nodos devolvieron HTTP200 y JSON idéntico, SH, con9 handlers Sablier y17 integrantes a10:13:17. Config.Group está vacío en9/9. Names es una lista: Karakeep tiene sus3 nombres en una sola cadena separada por comas. Normalizar cada entrada por comas/espacios conserva todos los miembros; la unión y los9 grupos coinciden entre nodos y con los archivos.

La copia de trabajo deVM208 también coincide, SHA1cf0d635…e722ae. El estático no es YAML general válido, pero no se atribuye una avería: el dialecto plano del parser real del controlador (código6ac25c0c…f268 idéntico aGit) procesa sus27 entradas y devuelve27 rutas/0lazy. No se modificó ese archivo. La proyección restringida de nombres quedó privada0600, sin configuración completa, en active-sablier-groups-20261020T081317Z.json, SHA . Esto acredita la protección conservadora observada, no cooperación de Sablier con el flock ni estabilidad futura de las rutas.

Root validó estructuralmente los11 contratos fuente con validate(), sus campos relevantes contra la proyección runtime, y los10 propietarios locales por archivo/servicio Compose. Un plan privado histórico sobre el inventario10:06, uniendo contratos, etiquetas y grupos activos contrastados, produjo19 entradas: 6 propuestas de restart,0start y13 manuales (11 sin clasificación suficiente y2 protegidas). plan_complete=false/restored=false. No se ejecutó ninguna propuesta. No es una autorización actual ni prueba de recuperación. Evidencia privada candidate-plan.json, SHA .

Se instaló únicamente el helper10:15:27 mediante reemplazo atómico, con perfil held exacto, lock, inactividad/procesos0, CAS de ambos archivos y metadatos. Bash5152d0d1 mantuvo bytes e identidad completa, incluida su notificación local. Backup de ambos originales y recibo: /root/rollback-nfs-inventory-adapter-s5g9n73i/. Instalador privado install_inventory_adapter.py, SHA ; 10 pruebas aisladas, plan local y revisión de diff correctos. No hubo rollback, notificaciones, ejecución manual del monitor ni acciones de contenedores.

La invocación nativa del CLI instalado10:15:48 devolvió schema1/media-208, 90 contenedores/0paused; EXPECTED_NFS y HELD confirmados con fuentes reales. Helper instalado: <REDACTED>. El planner y el ejecutor por IDs no se han desplegado; el monitor mantiene su baseline por nombres y su reparación retenida. Se están auditando los11 casos running sin clasificación antes de proponer cobertura automática adicional.

Primer ciclo automático posterior al helper nuevo:10:16:00→10:16:00, Resultsuccess/RC0; baseline actualizado10:16:00.564828 con19registros, sin pendiente ni contador. Timer active/waiting a10:16:33. No se forzó el ciclo.

Refinamiento de clasificación y cobertura del monitor (10:30 CEST)

El primer plan privado protegía conservadoramente cualquier lazy_names, incluso si lazy era la cadena false. Se corrigió antes de integrar un ejecutor: Trip Planner tiene tunnel.lazy="false", contrato always y ningún grupo activo Sablier con su nombre. Un nombre de grupo residual no equivale a lazy=true. La protección usa la unión activa Caddy, contratos on-demand y grupos de etiquetas sólo con bandera positiva; etiquetas no reconocidas no acreditan always.

Sobre el mismo inventario histórico10:06, el plan v2 produce7 propuestas de restart/0start y12 manuales (11 sin clasificación suficiente y1 protegida), restored=false. No ejecuta propuestas ni elimina el plan anterior. Artefacto privado candidate-plan-v2.json, SHA . La cadena false tampoco constituye una contradicción runtime con el contrato.

Los11 sin contrato de recuperación suficiente tienen propietarios coherentes con media/tools; no se acreditó una copia migrada entre esos11. La revisión de fuentes distingue nueve candidatos a disponibilidad permanente, Mylar3 sin señal explícita del monitor y OnePacerr con intención de pipeline pendiente. restart: unless-stopped por sí solo no autoriza una reparación. En procesos con colas, consumo/OCR o grabación, disponibilidad y admisión de reinicio son comprobaciones distintas. El arranque de nopor-ingest, por ejemplo, marca trabajos interrumpidos e inicia auto-drain según su configuración; eso no prueba trabajos actuales ni lo convierte en un servicio bajo demanda.

La omisión de Mylar3 se resolvió en el monitor, no en la política de arranque: fuente cd0c315 y lista instalada43 servicios, con publicación natural10:29 comprobada en Pushgateway y Prometheus. No se arrancó/reinició Mylar3 ni se usó la nueva lista como permiso de recuperación. La recuperación por IDs aún requiere diario por generación, ejecutor, admisión y migración del baseline.

Diario por generación preparado en fuente

Nuevo módulo nfs-recovery-journal.py, aún sin actuador ni integración: una única generación activa mantiene el manifiesto completo, sus hashes de manifiesto/política, metadatos legacy manuales y estado por ID. Exige disponer de todos los IDs del manifiesto; no permite sustituir generación, identidad, montajes o política para eludir intentos anteriores. Estados automáticos: ready→inflight→done/unknown. Al reabrir, inflight pasa a unknown; manual, done y unknown no vuelven automáticamente a ready. Un pendiente antiguo conserva sus bytes fuera del módulo; sólo se registra su hash como manual. Incluso con cero IDs, legacy mantiene el resultado parcial.

La persistencia exige callback del caller que verifique el flock existente, archivo0600/directorio privado0700, CAS de identidad completa/hash, temporal fsync, publicación exclusiva o reemplazo atómico, fsync de directorio y relectura. El directorio privado debe existir y haber sido persistido por su inicializador; crear un directorio nuevo requiere además fsync del padre antes de actuar. No crea otro lock. Cualquier excepción al guardar requiere releer/reconciliar, sin reintento ciego; si se intentó publicar se informa CommitUncertain. Esto sigue dependiendo de escritores cooperativos y fuentes de confianza; no excluye escritores privilegiados ni la ventana entre comprobar y reemplazar.

Once pruebas nuevas ejercitan disposiciones, inmutabilidad, intentos, pérdida del lock, enlaces/permisos, escrituras/fsync fallidos, cancelación a ambos lados del reemplazo y colisión de creación. Revisión independiente sin bloqueadores. done sólo representará la operación y comprobación limitada que aporte el futuro ejecutor, nunca salud completa de la aplicación. El módulo no ejecuta Docker, migra pendientes reales ni crea diarios en producción.

Compilador de política preparado en fuente

Nuevo módulo puro nfs-recovery-policy.py, sin descubrimiento ni acciones. Recibe proyecciones estrictas de contenedores/propietarios/contratos y de ambos Caddy; exige IDs completos y conjuntos de propietarios exactos, nodos270/271, observaciones completas, hash de configuración coincidente, endpoint Sablier conocido y ausencia de selectores Group. Normaliza Names por comas/espacios y comprueba la unión de nombres de ambas réplicas. No fija los actuales9/17 como límites de topología. Contratos remotos no autorizan homónimos locales.

Sólo primarios locales con coincidencia de archivo/servicio Compose son always; contratos on-demand/dependencias, Caddy y grupos lazy positivos son protección. Booleanos alineados con el controlador central:1/true/yes/on y0/false/no/off, con strip/case. False ignora nombres lazy residuales; valores desconocidos no conceden always. Los metadatos Compose ausentes de un contenedor no gestionado lo dejan unknown, sin invalidar a los propietarios válidos ni otorgarle permiso. Tipos o rutas no vacías malformados sí se rechazan.

Dieciocho pruebas nuevas cubren esos cruces, réplicas, propietarios, nombres compuestos, JSON y límites. La revisión detectó antes de publicar que el primer borrador trataba yes/1 como desconocidos, omitiendo proteger sus hijos sin etiqueta; corregido y contrastado con parse_bool del controlador central. El envoltorio incluye procedencia y execution_admission_proven=false. Sus flags validan las proyecciones, no prueban frescura, autorización del caller, colas inactivas ni coordinación con Sablier. Esta fuente y el diario todavía no están desplegados ni conectados al camino de recuperación Bash.

Verificación conjunta offline con las proyecciones reales retenidas10:06/10:13: el compilador produce exactamente la política v2 revisada; planner y diario aceptan19 entradas,7 ready y12 manuales, partial=true/restored=false. El diario no se persistió y no hubo acciones. Suite conjunta149 pruebas NFS en Mac (148 ejecutadas, una O_PATH nativa omitida); más15N-1 y6balloon:170 casos previstos por versión en Linux. La CI vigila ambos módulos nuevos en push/PR.

Vista de montajes de los contenedores: observación20oct10:50–10:53

Diagnóstico puntual de metadatos, sin instalar timer ni ejecutar comandos dentro de contenedores. Inventario Docker por IDs completos con Running/Paused/PID/ StartedAt y proyección Mounts Source/Destination/Type; procdir, namespace y raíz fijados mediante FDs, recorrido O_PATH|NOFOLLOW por componentes y selección del registro mountinfo por el mnt_id del FD real. Segunda apertura y relectura de tablas, identidad PID/namespace/raíz y proyección Docker después. Rechaza descender dentro de NFS/autofs intermedios; ambiguos quedan unknown. Sólo fstat de namespaces locales de proc, no getattr ni contenidos de archivos de aplicaciones. No usa exec/nsenter, no cambia montajes ni arranca/reinicia servicios.

Primera observación10:50:28,183902 CEST:90contenedores,19running NFS,0pausados, 24bindings. Cinco coinciden estrictamente con origen/tipo/raíz/dispositivo del NFS del host;17 difieren y2 quedan unknown. Segunda consulta10:53:04,242915 con recuentos de motivos conserva esos totales: los17 difieren simultáneamente en source y root, ninguno en tipo de filesystem. Los2unknown eran excepciones aún sin clasificar en esa versión. Host EXPECTED_NFS antes/después, Docker sin cambios observados, RC2 por observación incompleta y stderr vacío.

Eso NO acredita17montajes averiados u obsoletos: source es específico del filesystem y root es relativo a su raíz; distintas exportaciones pueden requerir un contraste adicional. Tampoco las cinco coincidencias prueban RPC, handles frescos, bytes originales ni salud de aplicaciones. No se modifica el planificador ni se conceden permisos de recuperación a partir de estos recuentos. Semántica mountinfo, vista del proceso, bind y submontajes.

Candidatos ejecutados por stdin y retirados al terminar, sin copia instalada: y . Ambos fijan el helper desplegado2f48e6a5;12/16pruebas sintéticas respectivas y revisión independiente de rutas de lectura/FDs antes del diagnóstico. Los límites de tiempo de usuario no garantizan interrumpir una espera kernel no interrumpible; las cotas Docker de salida se comprueban tras capturarla.

Tercera observación10:58:56,703509 CEST, candidato: conserva5coincidencias estrictas/17diferencias/2unknown. En los17, IP NAS y dispositivo coinciden con el host, y export+root concatenados por componentes son léxicamente iguales a los esperados. Ninguno es el caso de export padre estricto que se había considerado como hipótesis. Es evidencia de representación distinta; no demuestra un bind obsoleto ni equivalencia de handles/RPC. Se conserva mismatch, sin convertirlo en salud comprobada ni ordenar reparaciones. Los2unknown permanecen unknown_exception, sin atribuirles una avería.

Fuente reproducible: scripts/vm208/nfs-container-views.py, con20pruebas sintéticas (FDs y proc completamente simulados) y revisión independiente. Pruebas con guard hermano, sin rutas absolutas del Mac. Para la observación puntual revisada se puede pasar por stdin:

ssh media-208 'sudo -n /usr/bin/python3 - --execute' < scripts/vm208/nfs-container-views.py

Sin --execute sólo muestra el plan; no consulta Docker ni namespaces. Fija el hash del guard2f48e6a5 y falla como unknown ante otra versión. RC0 significa observación sin unknown, NO salud NFS; puede incluir diferencias conocidas. RC2 señala observación incompleta/unknown. No instala archivos, diario o cron ni cambia el monitor existente. La CI ejecuta los20casos junto al resto NFS.

La comprobación offline de la proyección histórica10:06 no encontró fuentes NFS no canónicas:30/30 válidas,24 en running; no explica los2unknown actuales y no prueba sus destinos, que esa proyección no contiene.

Una consulta adicional sólo a Docker,11:00:29,804974 CEST (ps+inspect en lote, sin abrir proc/namespaces), mantiene90contenedores/19running NFS/24bindings: 0fuentes/destinos no canónicos,0tipos distintos de bind y0afectados por esas validaciones previas. No explica los2unknown; siguen pendientes de diagnóstico dentro del recorrido de metadatos. No se repitió ningún arranque ni reparación.

Fuente publicada en main; CI 39397548870 correcta en Python3.10/3.12:15N-1+6balloon+169NFS=190casos por versión. Los20casos nuevos están dentro de169, no se cuentan dos veces. El código desplegado del monitor/guard sigue intacto; el diagnóstico es puntual.

Error ESTALE identificado20oct11:06–11:08

La instrumentación puntual de excepciones, tras23pruebas sintéticas y revisión de AST (sólo añadido de recuentos en catch existente), observó11:06:05,985430 CEST OSError/ESTALE en descend al abrir un componente O_PATH. Los recuentos se ponderan por TODOS los bindings del contenedor cuando se interrumpe su inspección: valor2 NO demuestra dos syscalls fallidas ni dos contenedores.

Consulta posterior11:08:05,368713 CEST, con25pruebas y descarte de afectados ante cambios Docker posteriores: un único ID afectado, correspondiente a onepacerr, con dos bindings marcados unknown. No es plundrio-onepacerr. Host EXPECTED_NFS antes/después, mismo inventario90/19running NFS/24bindings, 5coincidencias estrictas y17diferencias léxicamente compatibles como antes. Sólo se proyectaron ID/Name después para identificar al propietario; no se leyeron archivos de usuario, se ejecutaron procesos dentro ni se cambió estado.

Esto acredita el errno ESTALE en ese recorrido, no su causa histórica ni el estado completo del pipeline. Se investiga preservación de trabajos antes de cualquier recuperación. No se ha reiniciado el contenedor ni liberado el hold. Los candidatos instrumentados privados tienen hashes y ; no fueron instalados. La fuente reutilizable sólo añade motivos finitos unknown_estale/unknown_io/unknown_timeout; conserva unknown y el descarte por cambios de identidad. Nuevo fixture reproduce un contenedor/dos bindings y un único fallo: unknown2/matching0/unknown_estale2, sin inferir dos fallos.

Preservación de trabajos OnePacerr: revisión estática20oct11:25–11:32

Imagen actual8af9aac3 y revisión OCI, containerd369b6c8, running/no paused, StartedAt00:14:41.443288354Z,0restarts y sin healthcheck. Se leyeron únicamente archivos estáticos dist conocidos por imports de la imagen, a memoria/temporales privados, sin ejecutar código ni leer appdata/NFS. El archivo torrent.controller coincide con el patch propio SHA5ced1d del checkout stacks; no se equiparó upstreammain con esta revisión.

El nombre real del método es stoptWatching(): pone watchingfalse y borra handler, pero después procesa torrents completados, pudiendo copiar/unlink y cambiar categorías. No es una pausa inerte. gracefulClose() sólo termina con process.exit(0), sin esperar importaciones o pipeline. El informe en memoria getReport() no acredita que el importador independiente esté inactivo.

Express registra sólo HealthController/StatusController con GET de salud/estado; no se encontró operación de pausa/drain/quiescencia en esa superficie registrada. El prefijo API_BASE no se resolvió inequívocamente en la proyección permitida, por lo que no se supuso una URL ni se llamó a ninguna API. Actividad actual del pipeline desconocida; no se reinició OnePacerr ni se invocó stoptWatching. La recuperación pendiente debe conservar tanto pipeline como importaciones y evitar interpretar DONE o Docker running como admisión suficiente.

Diseño de pausa y conservación OnePacerr (fuente, 20 octubre, 14:05 CEST)

No se implementa todavía una pausa parcial ni una nueva API. El proceso actual sigue sin un contrato de drain acreditado; la recuperación ESTALE continúa pendiente. Esta revisión no consulta la aplicación, NFS, Docker ni sus datos.

Fuentes y alcance comprobado

La API oficial de GitHub identifica eltharynd/OnePacerr, revisión exacta <REDACTED> del label OCI ya observado. Se guardaron privadamente 42 blobs (143.403 bytes) de src, Dockerfile, package.json y configuración TypeScript, verificando cada SHA Git. No se instalaron paquetes ni se ejecutó el entrypoint. TypeScript 5.9.3 ya disponible reproduce byte a byte los cuatro dist capturados de index, Express, PipelineController y PipelineReport. Esto corrobora la revisión sin equiparar cualquier upstreammain con el runtime. Metadata/library se investigan desde ese commit corroborado, pero sus dist desplegados no se han leído ni verificado individualmente.

TorrentController sigue siendo el patch propio <REDACTED>. Además de resolver directorios, difiere de upstream en importación de bundles y hardlinks/copia. El diseño debe conservar esas diferencias, así como el cliente Transmission propio; no sustituirlos por una compilación upstream sin revisión.

Fuentes primarias fijadas: metadata, pipeline, torrent, biblioteca.

Riesgo reproducido y superficie completa

El reproducer local scripts/onepacerr-review/reproduce-drain-gaps.mjs carga sólo las tres clases dist con hashes fijados mediante módulos VM e imports sintéticos. No carga index/environment reales; cualquier fs/util de archivos se sustituye por rechazo y no se proporciona transporte de red. Tres casos pasan:

  • stoptWatching() pone watching=false y dispara processCompletedTorrents; no es una pausa sin trabajo nuevo.
  • Pipeline DONE/isRunning=false coexiste con una importación independiente que aún espera: consultar sólo el reporte no acredita quiescencia.
  • start() puede terminar dejando un episodio READY y un timer de retry vivo; esperar esa promesa no permite descartar la cola ni declarar todo terminado.

Son fixtures de comportamiento de esas clases con colaboradores sintéticos, no pruebas de actividad ni de quiescencia de la aplicación desplegada.

La cobertura necesaria incluye cuatro productores: refresh periódico de metadata, updates del WebSocket de metadata, watcher/importador torrent y pipeline/retry. sendToPipeline() genera metadata/NFO y lanza start sin await; el flag unprocessedMetadata puede quedar false mientras sigue ese trabajo. LibraryController añade escrituras y solicitudes bajo esos caminos. También hay callbacks de scans Emby/Jellyfin, listeners Plex y un reconnect Plex que no pueden sobrevivir ocultos a una supuesta cuenta de operaciones cero. Quitar un interval no cancela una callback async ya en vuelo. Un timeout de scan tampoco prueba finalización de la tarea remota.

Propuesta completa, antes de escribir la feature

  1. Admisión y ownership únicos. Un coordinador del mismo proceso mantiene OPEN/DRAINING/QUIESCED/UNKNOWN y generación. DRAINING se fija sin await antes de cerrar entradas: refresh/update, watcher, pipeline start/retry y nuevas importaciones. Las llamadas internas ya admitidas conservan un permiso explícito hasta terminar; timers futuros no heredan permiso válido de una operación que ya terminó. Se registran también las promesas hoy lanzadas sin await. No usar isRunning() como contador: PRE/READY son trabajo pendiente.
  2. Parada de productores sin trabajo final. Un método nuevo inerte detiene nuevas admisiones del watcher, guarda/limpia sus handles reales y evita rearmado al volver de await. No reutiliza stoptWatching. Metadata conserva actualización pendiente, desconecta su socket sin reconexión automática y registra cualquier callback ya activa. Se cancelan futuros retries de pipeline preservando los episodios READY y su próximo intento. Los clientes de biblioteca retiran listeners/polls/reconnect en finally y contabilizan sus callbacks activas; no cerrar un socket y permitir que close lo reabra.
  3. Drain finito sin forzar I/O. Se espera a que las unidades mutadoras ya iniciadas alcancen un límite seguro y sus hijos registrados terminen. No se interrumpe copy/unlink/cambio de categoría a mitad de unidad. Error ESTALE, timeout o cancelación dejan UNKNOWN con la admisión cerrada; nunca un recibo listo para reiniciar ni un retry automático. Un deadline no autoriza matar un proceso para afirmar drain. Una tarea remota de scan no confirmada queda registrada como no resuelta, no como completada por desaparecer el listener.
  4. Checkpoint local obligatorio. No existe actualmente un bind de estado local para OnePacerr: sus dos mounts de datos son NFS. Se requiere un directorio dedicado en appdata local, privado, independiente del NAS averiado, antes de habilitar esta feature. Schema versionado con generación, revisión de código/política y hash del cuerpo; metadata/unprocessed/firstRun, monitored/NFO necesarios, reporte actual, episodios restantes, historial y retry pendiente. Por operación: identidad estable, paso anterior/posterior y resultado done/unknown; no guardar secretos del entorno ni objetos socket/promesas. Cuerpo máximo propuesto16MiB, sin truncar historial/cola para caber: si no cabe se rechaza QUIESCED y se conserva el estado anterior para revisión. Retry registra intervalo e intención pendiente; no se persiste una lectura monotónica como si fuera válida tras reiniciar. Archivo0600, dir0700, temporal exclusivo, fsync archivo, rename, fsync del directorio y relectura. Fallo de escritura/fsync no produce QUIESCED; resultado de commit incierto exige relectura/conciliación, no reintento ciego.
  5. Reinicio cerrado y restauración explícita. El proceso nuevo carga y valida el checkpoint ANTES de library.init/metadata refresh/watcher. Inflight se reabre como unknown, no como ready; done no se repite. Metadata y PipelineReport se rehidratan como objetos propios, no se confía en métodos deserializados. READY/retry permanecen aparcados hasta una autorización explícita contra la misma generación. Una admisión repetida no duplica unidades o categorías. Para operaciones no idempotentes ambiguas hace falta conciliación del paso, no volver a copiar/unlink a ciegas. El drain no vuelve atómicas las operaciones destructivas históricas del importador ni restaura archivos ya eliminados.
  6. Recibo ligado al proceso. Sólo publicar QUIESCED si productores cerrados, operaciones/callbacks activas cero, ningún resultado desconocido sin resolver y checkpoint durable revalidado, ligados a PID/StartedAt/generación y hashes. Un estado readonly puede mostrar ese agregado; no equivale a una autorización de reparación NFS. Cualquier interfaz mutadora requiere canal local restringido y control de generación; no añadir un endpoint público sin autenticación.

Cambios previsibles: coordinador/checkpoint nuevo, index, metadata, pipeline, TorrentController propio y lifecycle de clientes de biblioteca, más tests de integración con colaboradores sintéticos. Es un cambio de aplicación y de persistencia, no una sola corrección de Compose. Debe revisarse antes de implementarlo y mantener los patches existentes byte por byte fuera del delta.

Bootstrap y aceptación que siguen bloqueando recuperación

Cambiar un bind JS no actualiza módulos ya cargados. Reiniciar ahora para cargar el gate nuevo perdería primero el READY/retry/metadata que se pretende conservar. El programa viejo no tiene exportación/rehidratación completa ni un drain acreditado. Por ello este diseño no autoriza recrearlo para instalar la protección ni demuestra una migración sin pérdida. Antes del primer despliegue hace falta una estrategia revisada de transición del estado del proceso viejo; los GET de reportes existentes, por sí solos, no cubren al importador.

Aceptación local necesaria antes de considerar ese despliegue: carreras de refresh/WebSocket/watcher/retry al cerrar gate; importer activo mientras pipeline DONE; start que retorna READY; tarea hija no awaited; callback async después de clearInterval; reconexión Plex; copia/rename/categoría en vuelo; fsync/rename y cancelación en cada lado del checkpoint; reapertura con inflight→unknown; dos peticiones de pausa/reanudación de la misma generación; bootstrap con checkpoint incompleto o incompatible; preservación exacta del READY/retry e invariantes de los patches propios. Todo con temporales locales y sin productos. No hay todavía feature implementada, endpoint, imagen construida ni despliegue.