Vista generada de dec-0111: ABI v0 de `styx-client-core`: core sin heap, longitudes `u32`, ubicación y bindings
docs/decisions/dec-0111-client-core-abi-v0-modelo-de-memoria.mdVista generada desde
docs/decisions/dec-0111-client-core-abi-v0-modelo-de-memoria.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 | PROPOSED |
| Fecha | 2026-09-28 |
| Fichero | docs/decisions/dec-0111-client-core-abi-v0-modelo-de-memoria.md |
Por qué importa (del frontmatter del ADR):
Precondición de track/client-platform-foundation/c1 (TKT-044 M0: 'paquete nuevo ... decisión per TKT-040 C0 boundary') y de c2 (C header + TS binding + lifetime tests). docs/design/client-core/04-abi-memory-rules.md aplica este ADR regla a regla.
Nodos del roadmap que lo citan en refs: ninguno.
Páginas de la documentación que lo citan: Clientes y player propio (especificado)
Leído de docs/decisions/dec-0111-client-core-abi-v0-modelo-de-memoria.md, el fichero canónico.
styx-client-core: core sin heap, longitudes u32, ubicación y bindingsAskUserQuestion a waxin. Nada de esto está implementado:
C0 es diseño y el código empieza en C1. Todas las opciones son reversibles hasta que C2 publique
el primer header.dec-0073), dec-0077 (el allocator no cruza la ABI), dec-0078
(una sola versión de fixtures), dec-0074 (estabilidad), r07 (orden FFI), r58 D6.3
(dec-0084, presupuestos de memoria Pi 3). D8 cita además dec-0070 (handoff), dec-0094 +
r61 §D1 (SessionStyxCapsule) y r61 D4 (generation especulativa).docs/design/client-core/04-abi-memory-rules.md.r56 D4.2 fija las reglas de la ABI v0 (handles opacos, enteros de ancho fijo, buffers del caller, el allocator no cruza la frontera, negociación de versión) pero deja abiertas cinco cosas que C1 necesita para escribir la primera línea:
RangeSet es estado de tamaño variable que vive entre llamadas. Si el core lo aloja con su
propio allocator, el caller tiene que devolvérselo para liberarlo: es exactamente el ownership
que dec-0077 prohíbe que sea ambiguo. En wasm32 además hay que decidir quién gestiona la
memoria lineal (la POC de bindings usaba un bump sobre __heap_base sin dueño declarado).size_t, que mide 32 bits en wasm32 y 64 en
aarch64/x86_64. Choca con la regla "fixed-width integers" del mismo ADR y con la igualdad
byte a byte de fixtures entre targets.native/zig/client-core/ o packages/client-core/) y método de
bindings TS. 00-INDEX.md los daba como "locked cont. 42 AskUserQuestion", pero no hay
registro de ese lock en docs/progress-log.md ni en un ADR. La POC de bindings
(08-ts-bindings-poc) dice
expresamente que su recomendación no es un lock.styx_plan_validate_v1. r56 dice "bytes" y no dice cuáles.last_issued, el mayor id emitido en la sesión, para que una especulativa descartada (r61 D4)
no vea reutilizado su id. dec-0070 transfiere sólo la generation activa y SessionStyxCapsule
(r61 §D1, dec-0094) lleva un único campo generation. Con sólo active, el receptor de un
handoff emite desde active y puede reutilizar el id de una especulativa descartada antes del
handoff: sus resultados tardíos se aplicarían como si fueran del receptor.| A. Core sin heap: el caller aporta el storage (recomendada) | B. Allocator interno del core + *_destroy | C. Allocator del caller inyectado por la ABI | |
|---|---|---|---|
| Ownership | único: el caller es dueño de toda la memoria | partido: el core aloja y el caller dispara la liberación | cruza la frontera |
dec-0077 | cumple por construcción | cumple en letra, deja un dueño doble | lo viola |
wasm32 | la memoria lineal la gestiona sólo el caller | dos gestores sobre la misma memoria lineal | callbacks JS→WASM por cada alloc |
| Presupuesto Pi 3 (r58 D6.3) | el cliente lo calcula antes de llamar | depende de la carga | depende del caller |
| Coste | una llamada *_storage_size(capacity) por tipo | ninguno visible | contradice r56 |
A. uint32_t en toda la ABI (recomendada) | B. size_t como en el ejemplo de r56 | C. uint64_t | |
|---|---|---|---|
| Ancho fijo | sí | no (32/64 según target) | sí |
| Fixtures byte a byte | idénticos | difieren en structs con longitudes | idénticos |
| Límite práctico | 4 GiB por llamada; el core sólo ve metadata de control y lotes de chunks | — | innecesario en wasm32 (memoria lineal de 32 bits) |
D1 — Core sin heap (opción A). Ninguna función exportada aloja memoria. Los tipos con estado
(StyxRangeSet, StyxIntervalMap) se inicializan dentro de storage que aporta el caller:
styx_range_set_storage_size(capacity, *out_size) → el caller reserva out_size bytes con
alineación 8 → styx_range_set_init(storage, storage_len, capacity, **out). El handle es un
puntero a ese storage; liberar es liberar el storage. Si una operación necesita memoria
temporal, el caller pasa un scratch y el core usa un FixedBufferAllocator local a la
llamada que muere al retornar: hay allocator dentro de la llamada, pero ni cruza la frontera ni
sobrevive a ella.
Cada función con scratch tiene una hermana *_scratch_size(input_len, *out_size) que da una
cota dependiente sólo de input_len; la función comprueba scratch_len contra esa cota antes
de leer la entrada, así que E_BUFFER_TOO_SMALL por scratch no depende del contenido.
D2 — Dos únicos estados globales del módulo, ambos de tamaño fijo y escritos sólo bajo la
regla de un solo hilo (r56 D4.2 "no concurrent callbacks"): el latch de versión negociada y el
registro de último error (styx_read_last_error). No hay más var globales. Los dos son de
la capa abi/: el latch se declara en abi/version.zig y el registro, junto al enum
StyxStatus, en abi/status.zig; L0, L1 y L2 no los importan.
D3 — Longitudes y capacidades uint32_t (opción A), incluida la firma de
styx_plan_validate_v1 (uint32_t input_len). Es la lectura de r56 que cumple a la vez sus
reglas "fixed-width integers" y "fixtures idénticos". Los punteros siguen siendo punteros:
miden 32 o 64 bits según el target y no aparecen dentro de structs POD que viajen en fixtures.
D4 — wasm32: memoria importada. El módulo se enlaza con import_memory; la
WebAssembly.Memory la crea y la gestiona el binding TS, que es el dueño de toda la región por
encima de __heap_base. El core no exporta ningún allocator.
D5 — Ubicación native/zig/client-core/ (build propio, sin dependencias del daemon).
native/zig/CLAUDE.md ya reserva ese nombre y native/README.md le da la misma misión.
packages/ es TS y sus reglas de dependencias no aplican a Zig.
D6 — Bindings v0: zig translate-c para verificar el header + glue JS/TS escrito a mano,
la opción 1 de la POC (cero dependencias nuevas, cada offset auditable, cubierto por los
lifetime tests de C2). jco/Component Model se reevalúa cuando la superficie supere ~30
funciones.
D7 — Entrada de styx_plan_validate_v1 = JSON UTF-8 producido por el control plane a partir
del plan ya validado por su schema TypeBox de @styx/api-contracts (dec-0116). El core lo
tokeniza sin heap (scratch del caller) y comprueba invariantes algebraicas; no re-declara el
schema. Un formato binario obligaría a mantener un segundo codificador TS a la par del schema
TypeBox, y la validación de plan es una llamada por plan, fuera de toda ruta caliente. Se
reabre sólo si el benchmark de C2 lo pone en un camino por chunk.
D8 — El handoff y SessionStyxCapsule transportan last_issued además de active
(amend propuesto de dec-0070 y dec-0094/r61 §D1). El controller de origen descarta su
especulativa pendiente, transfiere active y last_issued, y el receptor emite desde ese
last_issued. SessionStyxCapsule añade lastIssuedGeneration: LogicalGenerationId junto a
generation. La semántica de dec-0070 (transferir, no crear; el receptor hereda) no cambia:
se transfiere un entero más.
A. Transportar last_issued (recomendada) | B. Namespacing por controller | |
|---|---|---|
| Toca locks | amenda dec-0070 y dec-0094 (un campo) | ninguno |
| Forma | u64 monotónico, igual que hoy | u64 = controller_epoch (bits altos) ‖ secuencia (bits bajos); cada controller emite en su propio espacio |
| Handoff | el receptor sigue la misma secuencia | el receptor necesita un controller_epoch nuevo y único en la sesión: alguien tiene que asignarlo, que es otra vez un creador de ids |
admit | tagged > last_issued detecta ids no emitidos | el orden entre espacios deja de significar "más reciente"; is_stale sigue funcionando, E_GENERATION_FROM_FUTURE sólo detecta dentro del espacio propio |
| Coste | un campo en la cápsula y en el mensaje de handoff | reparto de bits (límite de handoffs y de ids por controller) y un asignador de epochs |
B evita el amend pero introduce un segundo creador (el asignador de epochs), contra el
single-creator de dec-0069. A mantiene un único creador activo en cada momento.
04-abi-memory-rules.md sustituye "ArenaAllocator interno" por D1 y size_t por uint32_t.init sin deinit, y storage más
pequeño que *_storage_size → E_BUFFER_TOO_SMALL.dec-0073.dec-0070 y dec-0094 y el campo a la
interfaz de SessionStyxCapsule cuando W1 la cree en @styx/api-contracts. Mientras D8 no
tenga lock, el invariante 1b de 08 se garantiza dentro de un controller y no a través de un
handoff.dec-0074).dec-0072).