IEEE-754 standardizes basic arithmetic, but not transcendental functions like sin(x). Across 44.65 million coordinate tuples, native glibc and WebAssembly libm disagreed on the last bit of sin(angle) in 3.104% of evaluations. No topology decision flipped, but the expected flip rate was not zero. AnankeLabs closed the gap by moving every runtime onto one pinned software sine.
Part I of the Kernel Hardening Triptych replaced a replay-and-rollback resolver with a domain proof and made paradox rejection terminal over operator overrides. The next campaign targeted cross-platform determinism in procedural topology generation.
Agents in KAIROS move across a discrete lattice of nodes , where is time (advancing by one on every evaluation tick) and is an integer spatial position. From any node, an agent has three candidate moves: left, stay, or right. The topology determines which of those moves exist. A deterministic hash of the node coordinates blocks each of the three outgoing edges with probability 0.15, leaving the available moves encoded as a three-bit movement mask; if all three edges are blocked, a separate rescue hash forces exactly one edge open. The lattice also contains attractors (goals carrying positive mass) and repulsors (hazards carrying a fixed mass of −1). While the engine’s Mulberry32 random number generator determines where those special nodes appear, the topology hash sets each attractor’s mass. Because those values govern both where agents can move and what pulls them across the field, two runtimes that construct different topologies from the same seed are simulating different worlds.
Within a single KAIROS evaluation tick, procedural topology sits at the foundation of the three-layer Rust evaluation kernel:
| Kernel Layer | Role in the Evaluation Tick | Triptych Installment |
|---|---|---|
| 1. Procedural Topology Generation | Hashes salted (t, b, seed) grid coordinates to decide which edges are blocked, which rescue direction applies, and the mass of each attractor | Part II (this article) |
| 2. Reachability Kernel & Cache | Propagates forward reachability fields across that topology and the committed causal locks, reusing earlier frames where it can prove them valid | Part III |
| 3. Causal Resolver | Evaluates candidate moves against stability floors and reports paradoxes, which the gateway turns into RejectParadox | Part I |
KAIROS compiles from a single Rust workspace (pinned to rustc 1.94.0 in rust-toolchain.toml) to native x86_64 and aarch64 binaries for server and edge deployment, and to wasm32-unknown-unknown (built with wasm-pack 0.13.1) for browser and isomorphic execution, accompanied by a standalone TypeScript reference verifier. For an audit receipt generated on an edge robot or a cloud node to verify bit for bit inside an offline browser inspector, every target runtime must construct the exact same topology from the same seed.
The Transcendental Blind Spot in IEEE-754
While IEEE-754 mandates correctly rounded results for addition, subtraction, multiplication, division, and square root (guaranteeing bit-identical results across conforming hardware under the same rounding mode), it only recommends correct rounding for transcendental functions such as , and standard math libraries do not provide it.
The KAIROS topology hash maps integer grid coordinates (t, b), a salt, and a 32-bit seed through four stages:
It forms the angle as a fixed linear combination of t, b, and the salt plus the seed itself, evaluates f64::sin(angle), multiplies by the scaling constant 43758.5453, and extracts the fractional part as a continuous hash in . Each salt governs a specific topological decision:
| Salt | Decision | Thresholds |
|---|---|---|
| 101, 202, 303 | Is this edge blocked? | 0.15 |
| 999 | Which rescue direction applies (stay, left, right)? | 0.5, 0.75 |
| 777 | What mass does an attractor carry (1, 2, 5, or 10)? | 0.5, 0.8, 0.95 |
Because special-node placement is driven by the Mulberry32 random number generator rather than this hash, any ULP drift in f64::sin(angle) that pushes a hash value across one of the thresholds above directly alters an edge mask, a rescue direction, or an attractor mass.
On native Linux targets, Rust’s f64::sin delegates to the host C math library (glibc libm). On wasm32-unknown-unknown, where no host C library exists, Rust’s compiler-builtins links a software implementation descended from FreeBSD’s msun and Sun’s fdlibm. In JavaScript runtimes, Math.sin delegates to whatever implementation the host engine embeds.
When we probed native Linux (glibc 2.36 and 2.43), WebAssembly, Node.js 22.19.0, and headless Chromium 145, Firefox 146, and WebKit 26, those six runtime targets separated into four distinct numerical families:
| Runtime / Target | Underlying | Family (pre-fix) |
|---|---|---|
Native Linux (x86_64, aarch64) | System glibc libm | A |
WebKit 26 | Engine built-in, bit-identical to glibc on every probe | A |
Rust wasm32-unknown-unknown | compiler-builtins (fdlibm lineage) | B |
Firefox 146 | Engine built-in, bit-identical to compiler-builtins on every probe | B |
Node.js 22.19.0 | V8 12.4 built-in | C |
Headless Chromium 145 | A newer V8 built-in | D |
We selected Family B as our convergence target because WebAssembly already used it, allowing us to unify every runtime without altering a single existing WebAssembly output. JavaScript’s built-in Math.sin, by contrast, spans all four families depending on the host engine. Although Node and Chromium both embed V8, they ship different V8 branches that disagree with one another: Chromium’s sine nearly matches glibc (differing on 30,306 V1 tuples), whereas Node’s differs from glibc on 1,482,273 tuples and from WebAssembly on 412,344. Without needing to diagnose the internal V8 divergence, that spread alone ruled out Math.sin and motivated a direct TypeScript port of the Family B routines, as detailed in the convergence section below.
Measuring the Divergence Across 141 Million Tuples
To determine whether a one-ULP disagreement in sin(angle) (where a ULP, or unit in the last place, is the least significant mantissa bit of an IEEE-754 f64) could flip a topology decision, we constructed a multi-layer audit corpus:
- V1 (primary grid): 44,651,520 tuples covering , , 16 seeds, and the five production salts.
- V2 (boundary and random): 1,048,576 tuples, comprising 141,120 tuples from a cross-product of boundary values (including extreme
i32coordinates and edge-case seeds) alongside 907,456 SplitMix64-generated tuples. - V3 (threshold neighbourhoods): 95,267,257 unique tuples. We collected every tuple on which any two runtimes disagreed, plus the 32 nearest tuples on each side of every (salt, threshold) pair. Every tuple in that base set (1,694,211 in all) was expanded into its neighbourhood: for every , with the salt unchanged, giving 125 tuples per base tuple. Neighbours outside the 32-bit ranges were discarded rather than wrapped (15,604,631 of them), and removing duplicates left 95,267,257 unique tuples.
Across V1, V2, and V3, the sweeps total 140,967,353 tuples, with the layers overlapping by design. Alongside them, a V4 layer holds 24 exact boundary cases (the predecessor, exact value, and successor of each threshold), and a V5 layer holds 8,930,304 movement masks, one per V1 cell.
Comparing native glibc (Family A) against WebAssembly (Family B) across those sweeps revealed the exact shape of the drift:
- Raw
sin(angle)mismatches: 1,386,083 of 44,651,520 V1 tuples (3.104%), each differing by exactly one ULP. In V3, which is concentrated on disagreements by construction, mismatches accounted for 3,194,081 of 95,267,257 tuples (3.353%). - Mismatches surviving to the hash: 1,130,551 V1 tuples (2.532%) still differed after scaling and taking the fractional part, with a maximum shift of and a mean shift of .
- Decision flips: zero across every runtime pair and every layer. The closest any hash came to a decision threshold was (a V3 tuple near the 0.75 rescue threshold), roughly the maximum observed shift.
In that comparison, the mismatch rate comes from V1, which is representative, whereas the closest approach comes from V3, which we built specifically to hunt for worst cases. While zero observed flips across 141 million tuples would close the ticket in a consumer application, a safety kernel has to establish the expected flip rate rather than asking whether a flip has happened yet.
Under a uniform distribution near the thresholds, a hash lands within distance of a given threshold, on the side the shift moves it across, with probability . Salts 101, 202, and 303 have one threshold each, salt 999 has two, and salt 777 has three; because V1 contains the five salts in equal numbers, each hash is compared against thresholds on average. Combining the share of V1 evaluations whose hash differs between the two runtimes (), the mean size of that difference (), and yields:
That rate corresponds to one divergent topology decision in roughly evaluations. Even if every mismatch shifted by the worst-case , the product is
or about (one per evaluations). This is a sizing estimate, not a guarantee: it assumes hash values are evenly spread near the thresholds, and we did not test that assumption. Because an engine evaluates many hashes per tick and an operational fleet runs for years, trillions of hash evaluations are routine. The moment a single threshold flips, one runtime blocks an edge or assigns an attractor mass that another runtime does not, causing every downstream reachability frame computed from that topology to diverge.
Converging Every Runtime on Family B (libm 0.2.16)
Inspecting the Rust 1.94.0 toolchain source revealed that the sine routines compiled into compiler-builtins for wasm32-unknown-unknown are byte-identical to the standalone Rust libm crate at v0.2.16 (even though the bundled copy’s internal manifest still reads 0.2.15). That equivalence let us converge every runtime onto Family B in two steps:
- Pin
libmand route native builds through it. Incrates/kairos-engine-core/Cargo.toml, we declaredlibm = { version = "=0.2.16", default-features = false }, locking the version and checksum inCargo.lock. Routing the topology hash throughlibm::sin(angle)on all targets moves nativex86_64andaarch64off hostglibc(Family A) and onto the exact software sine already compiled into WebAssembly. Settingdefault-features = falseinCargo.tomlis not sufficient on its own, because ordinary native builds still activatelibm’sarchfeature through an unrelated transitive dependency chain (rsa→num-bigint-dig). Our source audit confirmed thatarchonly touchesfloor, which is exact; what actually protects bit identity is that source audit, the corpus equality checks below, and the rule that any change tolibmor the toolchain triggers full requalification. - Port the sine implementation to TypeScript in place of
Math.sin. The TypeScript reference verifier now embeds a direct scalar port of the sevenlibm 0.2.16routines required bysin:sin,k_sin,k_cos,rem_pio2(Cody-Waite range reduction),rem_pio2_large(Payne-Hanek),floor, andscalbn. The port relies exclusively on operations that the ECMAScript specification defines bit-exact: IEEE-754+,-,*,/, 32-bit signed integer bit shifts and masks,Math.trunc, and big-endian word extraction viaDataView.
Validating the Port and Native/WASM Parity
To hold across the entire input domain, the port must reproduce both multi-word Payne-Hanek range reduction () and the kernel minimax polynomials without a single rounding discrepancy:
- Stage digests: Every intermediate calculation stage (angle, sine, scaled product, and final hash) is serialized as a stream of
u64IEEE-754 bit patterns, hashed with SHA-256, and verified against a frozen oracle. - Runtime matrix: Both the TypeScript reference verifier and the production WebAssembly module were executed under Node
22.19.0, Chromium145, Firefox146, and WebKit26, and cross-checked against nativex86_64(aglibc 2.43workstation and aglibc 2.36Debian Bookworm container) as well as nativeaarch64(linux/arm64on an Apple M1 Pro). - Corruption controls: To verify that the harness itself cannot silently pass bad data, the comparator is tested against flipped bits, removed or duplicated tuples, inverted mask bits, tampered provenance metadata, and altered boundary inventories. Across the 19 gate rows of the final binding run, we executed a designed set of 405 controls: 368 corruption controls (each copying a passing row, applying one specific corruption, and requiring the gate to reject it with its named diagnostic), 22 positive controls on unmodified copies (which must pass), and 15 usage controls.
Across the full V1, V2, V3, V4, and V5 layers, native x86_64, native aarch64, WebAssembly, and the TypeScript verifier now match with zero bit differences at every intermediate stage and every topological decision. Stage digests and masks cover every runtime, including the TypeScript verifier; the 26 trace fixtures (14 CLI scenario baselines and the six paradox pairs from Part I), which need the full engine, are regenerated byte for byte on native and WebAssembly.
In scope terms, this result establishes exact bit agreement across a qualified, finite corpus and runtime matrix for topology generation, rather than a universal claim across arbitrary unverified hardware, arbitrary inputs, or non-topology fields of the evaluation trace.
Performance: Native Range Reduction and the opt-level = "z" Trap
Moving from a host C library call to a pinned software sine carries different performance profiles on native silicon and in WebAssembly. Throughout the comparisons below, “legacy” refers to the pre-fix build: Rust’s standard f64::sin (glibc on Linux, Apple’s libm on macOS) for native targets and the sysroot compiler-builtins copy for WebAssembly, whereas the candidate build calls libm::sin.
Native (release-native profile). On ordinary small-angle inputs, the action gate ran at 1.018× legacy time on x86_64 Linux, 1.003× on M1 Linux, and 1.043× on M1 macOS (where the legacy baseline is Apple’s libm rather than glibc). Across both M1 targets, the motion gate remained between 0.993× and 1.029× of legacy time.
Large angles are the common path in production rather than a corner case. Within V1, , , and the salt together contribute at most about 71,000 to the angle, so whether a tuple crosses the Payne-Hanek threshold depends almost entirely on its seed. The seed enters the angle directly, so any seed above about 1.65 million (more than 99.9% of all 32-bit seeds) takes the large-angle path from tick zero. In our V1 grid, 9 of the 16 seeds do (the 16 seeds deliberately include 32-bit boundary values 0, 1, 2, 42, 1337, 65535, 65536, values around and , and two bit patterns, leaving above the threshold), which is why 25,116,480 V1 tuples (56.3%) take it. Even when starting from a tiny seed such as 42, advancing time reaches the Payne-Hanek crossover near tick 124,000, or roughly 21 minutes of operation at 100 Hz.
On that large-angle path, the software sine ran at 1.04× to 1.71× legacy time on aarch64, adding an estimated 50 to 108 ns per hash. That estimate divides the gate-level slowdown by the number of large-angle hashes; it is a derived average, and the sine itself was never timed in isolation. Native performance was governed by a pre-set bar of 1.10× legacy time on each measured workload. While ordinary inputs stayed well inside that budget, ten large-angle workloads exceeded our pre-set 1.10× bar; our internal review accepted them as explicit exceptions in exchange for cross-target bit identity, with identical decisions and gate responses either way.
WebAssembly. When we benchmarked our first candidate WebAssembly build against the legacy sysroot compiler-builtins baseline, latency regressed sharply across all three workloads:
- mask generation (
getAllowedMovesover a three-seed context rectangle) slowed by +55.6% - batch stage evaluation over 4,240 tuples slowed by +28.3%
- scalar evaluation slowed by +7.6%
Tracing the build graph explained the slowdown: previously, calling f64::sin on wasm32-unknown-unknown linked the prebuilt libm object bundled inside the toolchain sysroot’s compiler-builtins. Declaring libm as a direct Cargo dependency of kairos-engine-core caused Cargo to compile libm from source under our workspace release profile, which optimizes aggressively for small WebAssembly binary size (opt-level = "z", lto = true, codegen-units = 1, strip = true).
To restore speed optimization specifically for the math kernel without bloating the rest of the workspace, we added a per-package profile override in config/wasm-release.toml and passed it to the production WebAssembly build:
[profile.release.package.libm]
opt-level = 3
strip = "symbols"
Including strip = "symbols" alongside opt-level = 3 is essential, because overriding opt-level on a single package otherwise causes Cargo to fall back to stripping only debug information for that crate. We verified that the per-package override took effect by inspecting the emitted rustc command lines across all 20 wasm32 crates in the build graph. Across 9,900 benchmark samples spanning 30 processes, the production WebAssembly workloads then ran at 1.006× (scalar), 0.975× (batch, 2.5% faster than legacy), and 0.976× (mask, 2.4% faster than legacy). Changing the optimization level must not change results, and it did not: the rebuilt WebAssembly packages were requalified against the frozen oracle on every row, with zero bit differences, and the production WebAssembly packages are byte-identical to the variant that was timed.
What Guards the Invariant Now
Keeping topology generation bit-identical across future dependency, compiler, or profile updates relies on a three-tier enforcement model:
- Automatic on every relevant merge request (the
ananke-metapipeline): Whenever an engine, Core, or testbed pin, a root CI or package input, or the scope checker changes, CI runs the native compact corpus (4,240 tuples, 856 frozen cells, and the 24 boundary classifiers) on a pinned Debian Bookwormlinux/amd64image. Native gate runs build with--locked, refuse compiler, flag, and profile overrides, and record the toolchain, which the verdict judges against1.94.0inrust-toolchain.toml. The gate also builds both WebAssembly packages with--config config/wasm-release.toml --locked(failing if the sine helper source orconfig/wasm-release.tomldiffers from its pinned SHA-256), runs the portable TypeScript reference in Node, runs the core bridge tests and the corruption controls, and compares the corpus output against the frozen oracle. In parallel,production/PINSpins the exactlibmdependency line, version0.2.16, and itsCargo.lockchecksum;production/archive.shrefuses any tree that diverges from those pins, treating any modification as a new identity requiring requalification. - On demand: Triggering a pipeline with
RUN_TOPOLOGY_BROAD=1(manually from the web UI or via a scheduled trigger) runs the broad shards covering the full V1 to V5 layers, the 14 CLI baselines, the paradox regression corpus, and the fullkairos-coreWebAssembly bridge suite. The full Bookworm cross-check (verify:topology:n2) is likewise triggered on demand. - Qualification steps outside CI (process-enforced whenever
libm, the toolchain, or the build profile changes): Executing the four-browser matrix (since CI runs no browser), thelibmfeature-closure audit, therustc-invocation inspection, and the nativeaarch64suite on Apple silicon is an engineering process rule we follow whenever a pinned build identity changes.
With the causal resolver proven in Part I and procedural topology bit-identical across runtimes in Part II, one major audit target remained: guaranteeing that whenever the engine reuses a cached reachability frame, that reused frame is bit-for-bit indistinguishable from a fresh solve. How we closed eight cache-identity vulnerabilities, and the benchmarking campaign required to measure a 53-to-349-nanosecond verification cost on non-turbo silicon, conclude the series in Part III: A Fast Answer for the Wrong Agent.
Related reading
- Proving Away the Retry Ceremony: Domain Algebra and Terminal PrecedenceKernel Hardening Triptych, Part I: How enforcing bounds on the stability parameters let a two-line proof replace a replay-and-rollback loop, and why a causal rejection now outranks every operator override.
- A Fast Answer for the Wrong Agent: Reachability Cache Identity and Honest Nanosecond BenchmarkingKernel Hardening Triptych, Part III: Eight ways a reachability cache could hand back a stale frame, and what it took to measure a 53-to-349-nanosecond verification cost without host noise.
- Mechanical Provenance: Release Engineering for Autonomous TrustWhy an infrastructure runtime for autonomous systems requires deterministic SemVer, dual-tag transactions, and semantic replay gates.
Questions, replication interest, or a partnership in mind? We read everything that comes in.
Get in touch