Vista generada de dec-0105: quic-zig upstream (fork = endel + capa 0.17) y MoQT draft-17 completo
docs/decisions/dec-0105-quic-zig-upstream-y-moqt-draft-17.mdVista generada desde
docs/decisions/dec-0105-quic-zig-upstream-y-moqt-draft-17.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-09-28 |
| Fichero | docs/decisions/dec-0105-quic-zig-upstream-y-moqt-draft-17.md |
Enmienda a: dec-0057
Por qué importa (del frontmatter del ADR):
Gobierna el pin del submodule
docs/references/quic-zig(qué lleva el fork y cómo se mueve), el pin de libxev que arrastra, y el wire MoQT denative/zig/media-daemon/moqt/(moqt_types.DRAFT,SUPPORTED_DRAFTS,negotiateDraft). Sin este ADR un executor volvería a cargar parches de Styx en el fork (estrategia r36) o bajaría el wire a un subconjunto de draft-17 con códigos locales.
Nodos del roadmap que lo citan en refs: track/byte-runtime/m4
Páginas de la documentación que lo citan: Puesta en marcha de un nodo (implementado), zkit, spire y conduit (implementado)
Leído de docs/decisions/dec-0105-quic-zig-upstream-y-moqt-draft-17.md, el fichero canónico.
AskUserQuestion).track/byte-runtime/m4 (tickets 05 R-09 y 07 R-12, reabre y cierra R-03).dec-0057 / r43. Supersedes: la estrategia de parches de r36.
Reafirma: dec-0016.docs/track/byte-runtime/m4/evidence/quiczig-migration-2026-09-28/.Hasta esta rama el submodule apuntaba a cf68ecc: un fork MKS2508/quic-zig que había
divergido de endel/quic-zig a base de parches propios (r36 lane 1, capa 0.17 de r43) y que
ya no seguía a upstream. Dos P1 del nodo m4 estaban bloqueados por ese pin, no por Styx:
SendStream.write_buffer
retenía el rango entero y no existía primitiva de capacidad (sendCapacity /
notifyWritable) contra la que dosificar.processFrame sólo miraba el mapa bidi). Además, el control stream multiplexado no
podía identificar qué request cancelar.Upstream sí tiene esas primitivas: streamSendCapacity, getSendStreamStats,
notifyWritable/onWritable, onStopSending, el reset correcto de uni streams, la
compactación de write_buffer al ACK y el codec moq/ con draft-17/18. waxin decidió migrar
a upstream y lockeó el wire draft-17 completo.
end y end_of_track según §9.15.DOES_NOT_EXIST, INVALID_RANGE,
NOT_SUPPORTED, EXCESSIVE_LOAD, …). Sin códigos locales de Styx en el wire: el 0x1000
de la ronda anterior se eliminó.moqt_decode.zig, LegacyFetch y el alias fetch_ok_alias, que ahora
es fetch_ok_end en cliente y escenarios.moqt_types.DRAFT = .draft_17 es el draft por defecto, y SUPPORTED_DRAFTS = &.{DRAFT} es la
lista que se ofrece y acepta.negotiateDraft elige sobre WT-Available-Protocols. Un peer sin cabecera recibe DRAFT.
Una oferta sin ningún draft que hablemos cierra la sesión con VERSION_NEGOTIATION_FAILED,
nunca se adivina..draft_18 a la lista, después de pasar la batería de
conformidad y los smokes con un peer draft-18. No se activa en esta rama.main de MKS2508/quic-zig es upstream endel/quic-zig más la capa mecánica 0.17 y,
encima, los fixes de bugs de upstream que upstream aún no ha mergeado. Nada de Styx. La capa
mecánica es: compat 0.17, PSSSignature.concatVerify, guard comptime de toolchain, pin de CI
y pin de libxev.git ls-remote, 2026-09-28): main y fix/pacer-wakeup apuntan los dos
a 5066ee4, que es e6468ca (upstream 6d42346 más la capa) más el fix del pacer de D6.
El pin del submodule en esta rama sigue en e6468ca: moverlo es la acción pendiente de D6.
2026-09-29: main = de0f676, que es 5066ee4 más fix/wt-poll-h3-swallow
(git -C docs/references/quic-zig rev-parse origin/main). El pin está en de0f676 (2a5cf5b).fix/* del fork.main encima de la capa 0.17, y el pin de styx se mueve a ese main
con OK explícito.endel/main lo absorbe y el commit
deja de existir en el fork.main sólo lleva, además de la capa 0.17, commits que van camino de upstream y
tienen su rama fix/*. Eso es lo que distingue esta política de la de r36.endel/main, re-aplicar la capa mecánica y los fix/*
que upstream no haya mergeado.legacy/cf68ecc-pre-upstream, sólo como vara de medir.MKS2508/libxev f5d3811, que es el port 0.17 de MKS (c1e223b)
más los parches del tag quic-zig-2026-09-26-2 de endel (#224 #240 #245 #246 #247).zig fetch.La migración mantiene quic-zig all-in detrás de las interfaces de transporte del daemon. No reabre la alternativa Rust.
closeSession nunca llega y el gate se cuelga.
p6-repro-sequential.txt): entre el 1 % y el 3 % en
e6468ca, 0/30 en cf68ecc y 0/200 con el fix.p6-closegate-*.json, 5 conexiones concurrentes): 4 de 600 en
e6468ca y 0 de 600 con el fix. Aquí sale menos porque el tráfico de las otras
conexiones despierta el loop.closeSession en sí nunca se cuelga: 0 en los dos pines, máximo 11 ms.p6-pacer-stall-diag-trace.txt): Connection.send rechaza un paquete por
pacing y nextTimeoutNs recalcula el retardo del pacer más tarde, al armar el timer. Si la
espera ya terminó entre medias, no se programa ningún despertar. Con nada en vuelo, el
siguiente timer es el keep-alive o el idle.e6468ca, 5066ee4. Está publicado en MKS2508/quic-zig
como rama fix/pacer-wakeup y como main (D3); comprobado con git ls-remote. La PR a endel
no consta desde esta rama.
send guarda el deadline absoluto del rechazo y nextTimeoutNs lo reporta.
onTimeout lo descarta cuando pasa.e6468ca (p6-quiczig-pacer-test-RED-e6468ca.txt). Con el fix la suite
del fork pasa 992/992 (p6-quiczig-suite-GREEN-fix.txt).5066ee4 y volver a pasar el gate de closeSession
(tools/soak-harness/scenarios/wt-nconn-rss.ts, 20 runs) sobre ese pin. El VERDE que hay
(p6-closegate-fix5066ee4.json) se midió con el fix en un árbol local, no con el pin movido.
No se mueve sin OK.5066ee4, el
main del fork. El gate se repitió sobre ese pin en el árbol integrado y sigue ROJO:
1/20 runs, 1 de 600 conexiones parada y 0 closeSession colgados
(docs/track/byte-runtime/m4/evidence/closegate-pin-5066ee4-2026-09-28/). El cliente parado
es de warm-up y se queda en la misma fase que los rojos de e6468ca: el servidor recibe el
CONNECT y crea la sesión, pero el cliente nunca recibe WT session ready. El fix del pacer
no cierra D6 del todo. Queda una parada residual de baja frecuencia sin root cause. Es el
follow-up m4#11-FU-closegate-connect-stall, que sigue la política de D3 (test en rojo y fix
en el fork). El bloqueo de abajo sigue en pie: sólo cambia el pin sobre el que se mide.de0f676.
WebTransportConnection.poll() devolvía null después de
tragarse un evento H3 sin significado, aunque H3 tuviera más eventos en cola. El 200 del
CONNECT se quedaba sin leer (closegate-rootcause-2026-09-28/).de0f676, con test rojo/verde en el fork. Ya está en main de MKS2508/quic-zig
siguiendo D3, y el submodule se movió a ese main (2a5cf5b).close_ms_max 6 ms
(docs/track/byte-runtime/m4/evidence/closegate-pin-de0f676-2026-09-29/).onReadable con una sola iteración: HUNG 2/30 en 5066ee4 y 0/30 en
de0f676.77c944f);styx-wt-client no salía cuando el peer cerraba la sesión (c9f93bf).de0f676 (y de 5066ee4) a endel/quic-zig, con OK de
waxin.e6468ca
(p6-closegate-e6468ca.json). La condición era que el pin estuviera en un main del fork que
contuviera 5066ee4 y que el gate pasara VERDE sobre ese pin. Se cumple con de0f676.poll_interval_ms), por eso allí el bug queda
acotado a 10 ms y no se ve. El WT byte path no tiene poll y se queda parado.e6468ca agotó una vez
el timeout en una transferencia de 1 MiB, y no se reprodujo en 10 repeticiones
(h3-debug-loop.txt). Queda sin atribuir.poll_interval_ms en el WT byte path como remedio: acotaría el síntoma y
dejaría el bug debajo.dec-0049 (arena → GPA), junto con r37/r38, cerró la retención G4: memoria del stack QUIC que
la arena nunca liberaba. Con N=250 conexiones WT (r03-wt-n250-*-summary.json, ReleaseSafe,
mismo daemon y escenario), el pin legacy muestra otra retención:
| pin | RssAnon tras warm-up → final | retención por conexión cerrada | bytes servidos |
|---|---|---|---|
cf68ecc (legacy) | 78 176 → 180 592 KiB, lineal | 409,66 KiB | 5,26 GB |
e6468ca (upstream) | 72 960 → 74 276 KiB, plana | 5,26 KiB | 10,78 GB |
live_bytes (el allocator contado del daemon) sólo sube unos 7,9 MB mientras
RssAnon sube unos 100 MiB. La retención está fuera de lo que el daemon cuenta.r03-moqt-one-conn-r4000-summary.json).r03-wt-oneconn-*-summary.json):
write_buffer retenido hasta cerrar la conexión: la observación de disposal de
r36, que el pin legacy mantenía.r03-wt-oneconn-growth-attribution.txt): los mapas por stream de quic-zig
y del handler no crecen. Lo que crece es ReadScheduler.reads, append-only, una entrada
por rango. Es deuda documentada de la sesión (session/scheduler.zig,
"scheduler-reads-monotonic-growth"), no de quic-zig.main.e6468ca (4/600) y en 5066ee4 (1/600),
y VERDE en de0f676 (0/600, 2026-09-29). El bloqueo de merge de D6 queda levantado con el pin
en de0f676.docs/decisions/README.md no se toca en esta rama, porque el índice lo mantiene axon. Hay
que añadir dec-0105 al integrar.