Introduction
Pulse Mesh™
The standard: two byte layouts, the codec that pins them, the ring, the store and the wire that carry them, and the bindings that prove them in other languages.
Pulse Mesh Standard · working draft · reference implementation
Pulse Mesh™ is a transport substrate built on two byte layouts. The Pulse is a 128-byte control signal that occupies two cache lines and moves through a shared-memory ring without a lock. The PulseEnvelope carries anything larger, at any size, and is addressed by the 16-byte BLAKE3 hash of its canonical bytes, so the same content produces the same identifier in every language that implements the codec. Everything above the two layouts is layered: an implementer can take the codec alone, the codec with the reference ring, or a full domain ring with its composed surface.
This repository is the standard: the two layouts and their canonical codec, the conformance vectors that pin their bytes, the SeqLock ring, the envelope store and the single-slot region that move Pulses through shared memory, the UDP transport that moves them between machines, the vocabulary crates (the Lexicon, the context and capability registries), the composition macro the rings build with, the pulse command line, three examples, and the C, Python, Node and WebAssembly bindings. It depends on nothing outside itself. Two repositories build on it: the daemon, a runner that routes envelopes between processes through a local port, and the profiles, the enterprise, HFT and robotics rings. Each has a book of its own, shaped like this one.
This book documents the reference implementation in this repository. It is organised in four parts, each answering one kind of question, and every page states at its top which kind it is and whom it is for.
The four parts
| Part | The question it answers | How to read it |
|---|---|---|
| Examples | How do I see it working? | Hand the prompt to your agent, or follow the steps; each ends with something running and checks you can run. |
| Workflows | How do I do my domain’s job with it? | An example’s shape for one domain, saying what is implemented and what is deferred. |
| How-to guides | How do I do this particular task? | Find the task, follow the steps. |
| Reference | What exactly is true? | Look things up. The specification and the generated tables live here. |
| Concepts | Why is it built this way? | Read for understanding. One invariant and one figure per page. |
A last part, Contributing, is for people changing the tree: the workspace, the gates, the decision record, and the documentation’s own rules.
Where to start
- Seeing Pulse Mesh work: Across a network encodes, verifies and sends Pulses over UDP with nothing but this repository; Two programs talk through the daemon and An agent swarm are in the daemon’s book.
- Building on it from another language or another process: Language bindings and the API reference; the ring and the store inside your own process is the first workflow.
- Understanding how the pieces fit: the workspace pages in the Reference part follow the repository’s two tiers, and the Concepts part says why they are shaped as they are and where the daemon and the rings sit above them.
- Contributing: Developer Onboarding, then the documentation style if you are writing.
How this book relates to the rest of the repository
Two records are authoritative, and this book defers to both. The rustdoc (cargo doc --workspace --no-deps --open) is the API contract: every crate opens with an overview, every public item is documented, and a workspace lint keeps it that way. The decision log under decisions/ records why the non-obvious calls were made, in order and without rewriting. The book is the guided path through that material. Where the book and the code disagree, one of them is wrong, and either way it is a defect to report.
What this book claims and what it does not
The book describes the tree at the version shown in the sidebar. It records no measured figures. Where it describes a measurement it gives the command that produces one. Where a behaviour is specified but not yet implemented, the page says so and names the test that will prove it. Refutations are welcome: the fastest way to improve a page is to show where it is wrong.
Onboard your agent
Start here · what a coding agent needs to set up Pulse Mesh, run it, and put other agents on the mesh
Pulse Mesh is built to be set up and driven by a coding agent. This page is what you hand it: one prompt to get running, what it needs to know before it changes anything, the MCP server that gives it a live mesh, and how agents talk to each other through one.
1. Paste this
Open an empty folder in your agent and paste:
Set up Pulse Mesh for me. In one empty folder, clone these three repositories side by side, from the GitHub account that publishes Pulse Mesh (the account this repository belongs to; if you are already inside one of them, its git remote names it):
pulse-mesh
pulse-mesh-daemon
pulse-mesh-profiles
Install Rust with rustup if it is missing; each repository pins its version in rust-toolchain.toml.
Then in each repository read AGENTS.md and run its "Prove the tree works" steps up to scripts/gate.sh --quick.
Finally show me it working:
1. in pulse-mesh, encode, decode and verify one envelope with the pulse command line;
2. in pulse-mesh-daemon, run pms demo;
3. in pulse-mesh-profiles, run the robotics example.
Explain in plain words what each step showed. Stop and tell me if anything fails.
2. What your agent needs to know
- Three repositories, side by side.
pulse-meshis the standard;pulse-mesh-daemonandpulse-mesh-profilesbuild on it and find it at../pulse-mesh. AGENTS.mdin each one is the agent’s brief: setup, the order of the first commands, a map of the tree, and the rules every change keeps.- What is true, in order: the specification, then the test vectors and the port protocol’s fixtures, then the rustdoc, then the decision records, then the rest of this book. When two disagree, one is wrong, and the agent should say so rather than pick one.
- Done means the gate passes.
scripts/gate.shin each repository is the check a change passes before it merges. - What it must never do: edit a test vector or a fixture to make a test pass, make a deferred feature look implemented, or rewrite an accepted decision record.
3. Give your agent the mesh: MCP
pms-mcp, in the daemon’s repository, is a Model Context Protocol server. With a daemon running, it lets an agent attach to the mesh, send envelopes, check health, query and replay what the basket captured, read a ring’s slots, and round-trip the codec on input it supplies.
cargo install --path tools/pms-mcp --locked # from pulse-mesh-daemon
claude mcp add pms-mcp -- pms-mcp # Claude Code; any MCP client that can launch a command works
Use the MCP server from an agent registers it with other clients and tries it by hand, and the MCP tools lists every tool’s input and output.
4. Put agents on the mesh
Agents coordinate the way any programs on the mesh do: each attaches to the daemon by name, subscribes to the channels it cares about, and sends envelopes that reach every subscriber attached at that moment. One agent can plan and others can work:
pms start --foreground # one daemon for the machine
pms tap --channel 'jobs/*' # a worker listens for jobs
pms push jobs/new 'summarise report.pdf' # a planner hands one out
An agent with MCP does the same through mesh_attach and mesh_send. One without it speaks the port protocol directly over the daemon’s WebSocket, through the Python or Node thin client or plain JSON frames. Every envelope is named by a hash of its content, so a receiving agent can check it got exactly what was sent. Two programs talk through the daemon walks through it by hand.
The daemon has no authentication and no encryption, so keep it on loopback, which is its default, when agents share a machine.
An agent swarm builds this in ten minutes: a planner hands out jobs, two workers answer, and every result is checked against its job by hash.
Next
- Developer onboarding: the same setup by hand, in five minutes.
- Use the MCP server from an agent: every client, and the server’s limits.
Developer Onboarding
Start here · five minutes from a fresh checkout to a tree you know works, by hand
Working with a coding agent? Start with Onboard your agent: one prompt sets up all three repositories. This page is the same setup by hand.
By hand
- Set up.
rust-toolchain.tomlpins the Rust version, andrustupinstalls it on the firstcargocommand. - See it work.
cargo install --path tools/pulse-cli --locked pulse versionpulse versionprints the version, the pinned toolchain and the identifier the codec computes for the reference test vector. When that identifier matches the one in the specification’s fixtures, the codec you built agrees with the standard. - Encode one envelope. Across a network encodes one, decodes it, shows the check fail on one changed byte, and then sends Pulses over UDP.
- Prove the tree.
scripts/gate.sh --quickruns the per-commit checks;scripts/gate.shruns the gate a change passes before it merges. A check whose tool is missing fails and prints the command that installs it.
From there, The ring inside your process puts the ring and the store inside one program.
Before your first change
CONTRIBUTING.mdsays what a contribution means during the beta, holds the table of every check the gate runs, and gives the four-question test for where new code goes, which Workspace Layout explains.AGENTS.mdlists the rules every change keeps, for a person as much as for an agent: never edit a test vector to pass a test,unsafeonly with a// SAFETY:comment, a doc comment on every public item, decision records added and never rewritten.- The specification is normative, the rustdoc is the API contract, and the Decision log records why the non-obvious calls were made. This book explains and guides; where it and the code disagree, one of them is wrong.
Where to read first
pulse/pulse-canonandcore/pulse-wire-udp: the two layouts, their codec, and the wire that carries them.core/core-ring/src/lib.rs, the SeqLock ring, andcore/core-store/src/lib.rsbeside it, the envelope store.tools/pulse-cli/src/main.rsandsrc/ops/: thepulsecommand line, the most concrete tour of what the standard does.pulse/pulse-spec/test-vectors: the bytes every implementation must reproduce; chapter 8 says how they are checked.
Three examples
Part opener · for anyone who wants to see Pulse Mesh work before reading about it
Each example ends with something running on your machine, and each is written to be handed to a coding agent as one prompt or followed by hand. Every step names the command and shows what it prints, and every example closes with checks you can run, what happened, and what the code does not do.
- Two programs talk through the daemon: start a daemon, publish from the command line, from two Rust programs, and from Python and Node, and watch every message named by its content.
- An agent swarm: a planner hands out jobs, workers answer: a planner and two workers on one mesh, every result checked against its job by hash.
- Across a network: encode, verify, send over UDP: one envelope in your hands, then Pulses over UDP between two terminals or two machines, with nothing but the standard.
The first two need the daemon’s repository; the third needs only the standard’s. Where an example lives in another book, its link goes there.
Across a network: encode, verify, send over UDP
Example · for anyone moving Pulses between two machines, or two processes that cannot share memory; about fifteen minutes with nothing but the standard
You will have: one envelope encoded, decoded and verified by hand, with the check failing on one changed byte; three Pulses sent over UDP from one terminal and received in another, on one machine or across two; and a program that does the same exchange a thousand times.
You need: the standard’s repository set up as Start here describes, with pulse installed (cargo install --path tools/pulse-cli --locked). Two terminals. One machine is fine: the loopback interface is a network as far as the transport is concerned.
Hand this to your agent
Run the example "Across a network" from book/src/examples/across-a-network.md in the pulse-mesh repository:
1. Install the command line if it is missing: cargo install --path tools/pulse-cli --locked. Run pulse version and show me the codec-test-vector line; its identifier must be 57f2c2eec9bae1b5e69c97f88e9bfd8d.
2. Write spec.json with {"envelope_type": 1, "payload_utf8": "hello, mesh"} and run pulse encode --json spec.json. Confirm decoded_equal is true and keep canonical_hex.
3. Run pulse decode on that hex and confirm verified is true. Then change the last hex byte from 68 to 69 and decode again: verified must be false and the exit status 1.
4. Start pulse recv 127.0.0.1:9000 --count 3 --timeout-ms 10000 in the background, then run pulse send 127.0.0.1:9000 --count 3 --payload 32. Show me the receiver's "received 3/3" line and its three seq lines.
5. Run cargo run --release -p example-02-loopback-udp-rust and confirm it prints a line starting with OK.
6. If I have named a second machine, repeat step 4 with the receiver bound there and the sender pointed at its address, and tell me what the receiver printed.
Explain in plain words what each step showed, including why the tampered envelope failed. Stop and tell me if anything fails, and say which step.
Or do it by hand
1. Check the codec
pulse version
pulse (pulse-cli) 1.0.0-beta
rust-toolchain 1.95.0
codec-test-vector 58 bytes, BLAKE3-16 57f2c2eec9bae1b5e69c97f88e9bfd8d
The last line is the one that matters: pulse has just encoded the reference test vector and printed the identifier it got. The specification’s fixtures record 57f2c2eec9bae1b5e69c97f88e9bfd8d, so a line that shows it says the codec you built agrees with them.
2. Encode an envelope
cat > spec.json <<'EOF'
{ "envelope_type": 1, "payload_utf8": "hello, mesh" }
EOF
pulse encode --json spec.json
canonical_size 61 bytes
content_hash 71aad243e722c17b9f1cf2723ec6148a
decoded_equal true
canonical_hex 71aad243e722c17b9f1cf2723ec6148a0000000000000000000000000000000000000000000000000100000000000b00000068656c6c6f2c206d657368
Four lines, four facts. The encoding is 61 bytes: the 50-byte fixed part every envelope has, plus your 11 bytes of payload. content_hash is the identifier, the first 16 bytes of a BLAKE3 hash over a fixed domain tag and the envelope’s content; the hex begins with it. decoded_equal says decoding the bytes gave back an envelope equal to the one encoded. canonical_hex is the whole envelope, and every conforming implementation, in any language, produces these bytes from this spec.
3. Decode, then change one byte
pulse decode 71aad243e722c17b9f1cf2723ec6148a0000000000000000000000000000000000000000000000000100000000000b00000068656c6c6f2c206d657368
envelope_type 0x0001
content_hash 71aad243e722c17b9f1cf2723ec6148a
verified true
monotonic_nanos 0
flags 0x0000
payload Raw, 11 byte(s)
verified true means the decoder recomputed the identifier from the decoded content and it matched the one the bytes carry. The last byte of the hex is the payload’s final h (68). Change it to i (69) and decode again:
pulse decode 71aad243e722c17b9f1cf2723ec6148a0000000000000000000000000000000000000000000000000100000000000b00000068656c6c6f2c206d657369
envelope_type 0x0001
content_hash 71aad243e722c17b9f1cf2723ec6148a
verified false
monotonic_nanos 0
flags 0x0000
payload Raw, 11 byte(s)
The bytes still parse, because the layout is intact. But the identifier they carry was computed from hello, mesh and the content now says hello, mesi, so verified is false and pulse decode exits with status 1, so a script can refuse the envelope without reading the output. That is the whole of content addressing: a name the bytes cannot keep once they change.
4. Receive and send over UDP
In the first terminal:
pulse recv 127.0.0.1:9000 --count 3 --timeout-ms 10000
It binds the address and waits, silently, for three datagrams or ten seconds. In the second:
pulse send 127.0.0.1:9000 --count 3 --payload 32
sent 3 pulse(s) (32 byte payload each) to 127.0.0.1:9000 in … µs
The time is your run’s own and changes with every run and machine. The receiver prints:
received 3/3 pulse(s) on 127.0.0.1:9000 (timeout 10000 ms):
seq= 0 send_ns= … inst=DATA envelope=32 B
seq= 1 send_ns= … inst=DATA envelope=32 B
seq= 2 send_ns= … inst=DATA envelope=32 B
Each line is one datagram: its sequence number, the sender’s clock reading when it was sent, the Pulse’s instruction and the envelope’s size. The sender framed each Pulse with a 32-byte envelope into one datagram, with a magic, a version, the sequence, the clock and a CRC around them, and the receiver checked each on arrival.
Across two machines: run pulse recv 0.0.0.0:9000 --count 3 --timeout-ms 10000 on the receiving machine, and pulse send <its address>:9000 --count 3 --payload 32 on the sending one. UDP port 9000 must be open between them; nothing else changes.
5. The same exchange in a program
examples/02-loopback-udp-rust is the exchange above in one process, a receiver thread and a sender, a thousand times:
fn main() {
let mut receiver = Receiver::bind("127.0.0.1:0").expect("bind receiver");
receiver
.set_read_timeout(Some(Duration::from_millis(2_000)))
.expect("set timeout");
let recv_addr = receiver.local_addr().expect("recv local_addr");
let recv_handle = std::thread::spawn(move || -> usize {
let mut got = 0usize;
for _ in 0..PULSE_COUNT {
match receiver.recv() {
Ok(_) => got += 1,
Err(e) if e.is_timeout() => break,
Err(e) => panic!("recv: {e}"),
}
}
got
});
let mut sender = Sender::bind("127.0.0.1:0").expect("bind sender");
sender.connect(recv_addr).expect("connect");
let mut pulse = Pulse::zeroed();
pulse.instruction = Instruction::Data as u16;
for i in 0..PULSE_COUNT {
pulse.monotonic_nanos = pulse_wire_udp::monotonic_nanos();
pulse.scope = i as u32;
sender
.send(pulse.monotonic_nanos, &pulse, PAYLOAD)
.expect("send");
}
let received = recv_handle.join().expect("recv thread");
assert_eq!(received, PULSE_COUNT, "all pulses must arrive on loopback");
println!(
"OK — {} pulses sent and received on {} ({} B payload each).",
PULSE_COUNT,
recv_addr,
PAYLOAD.len()
);
}
cargo run --release -p example-02-loopback-udp-rust
OK — 1000 pulses sent and received on 127.0.0.1:… (8 B payload each).
The port is the kernel’s choice and differs each run. send takes the sender’s clock reading, the Pulse and the envelope bytes, frames them, and returns the sequence number it used; it refuses an envelope larger than the transport’s maximum, because the transport never fragments a datagram unless asked to. recv_into waits for a datagram, checks its magic, version and CRC, and hands back the Pulse, the envelope’s length and the sender’s sequence and clock.
Check it worked
| Check | Command | Pass |
|---|---|---|
| The codec agrees with the specification | pulse version | The codec-test-vector line ends 57f2c2eec9bae1b5e69c97f88e9bfd8d |
| Encoding is canonical | pulse encode --json spec.json | content_hash 71aad243e722c17b9f1cf2723ec6148a, decoded_equal true |
| Tampering is caught | pulse decode of the hex with …68 changed to …69 | verified false, exit status 1 |
| Datagrams arrive and verify | pulse recv … --count 3 and pulse send … --count 3 | received 3/3, three seq= lines |
| The program agrees | cargo run --release -p example-02-loopback-udp-rust | A line starting OK — 1000 pulses |
If pulse recv prints received 0/3, the sender ran before the receiver was bound, or a firewall dropped the datagrams; start the receiver first and, across machines, open UDP port 9000. If pulse version shows a different identifier, the codec you built does not agree with the specification’s fixtures, and that is a defect to report.
What happened
- One canonical encoding. The envelope has exactly one byte layout, and its identifier is a hash of that layout. The same spec gives the same bytes and the same name in every language, which is what the conformance vectors pin and what
pulse versionchecks. - Verification is recomputation. A decoder does not trust the identifier it is handed; it recomputes it from the content and compares. A changed byte parses and fails.
- UDP carries the same bytes a ring would. One Pulse and its envelope per datagram, framed with a magic, a version, a sequence, the sender’s clock and a CRC. The receiver checks the frame and the envelope; a datagram that fails either is dropped and counted.
- Over loopback nothing is lost. Over a real link the transport has mechanisms for that, each off by default and switched on with two lines on each side: backpressure, so a sender does not outrun its receiver; retransmission, so a receiver can ask for a frame it missed; and parity, so a receiver can rebuild a lost frame without asking. Tune the UDP transport switches them on.
What the transport does not do by default: no encryption and no authentication of the sender; the CRC detects accidental corruption, not a forged datagram. Sealing is an option of the transport, and Trust boundaries says what each layer protects.
Make it yours
- Larger payloads:
--payload 1000onpulse send; the transport refuses an envelope larger than a datagram carries unless fragmentation is on. - A nested envelope:
pulse encodewith"envelope_type": 2and a payload that is itself an envelope’s hex, as the specification’s nested test vector does. - Your own program: the loopback example is thirty lines; swap
127.0.0.1for a peer’s address and you have a sender on one machine and a receiver on another.
Next
- Two programs talk through the daemon: the same naming, with a daemon routing between processes.
- Tune the UDP transport: backpressure, retransmission and parity.
- The UDP transport, the specification chapter, and Transport design, the reasoning.
- Run the smoke bench and read the result: the same exchange at two hundred thousand Pulses, timed, on your machine.
Spoken walkthrough
A script for the video and audio version of this page, about ninety seconds.
Start with one envelope. We write a spec, eleven bytes of payload, and encode it. Sixty-one bytes come out, and the first sixteen are the envelope’s name: a hash of its content. Decode those bytes and the decoder recomputes the name and compares. Verified, true.
Now change the last byte, an h to an i. The bytes still parse. But the name no longer matches the content, so verified is false and the command exits with an error. That is content addressing: a name the bytes cannot keep once they change.
Then the network. A receiver waits on a UDP port; a sender fires three Pulses at it. Each one is a Pulse and its envelope in one datagram, with a sequence number, the sender’s clock and a checksum around it. The receiver prints three lines, one per datagram, each checked on arrival. Put the receiver on another machine and change one address; nothing else changes.
Last, a program does the same exchange a thousand times in one process and prints OK. Thirty lines, and it is the whole transport. The daemon, in the next example, is built on exactly this.
One domain’s job, end to end
Part opener · for anyone putting Pulse Mesh to work in robotics, enterprise messaging, high-frequency trading or distributed compute
A workflow has an example’s shape and one domain’s job: what you will have, one prompt for your agent, the same steps by hand with what each prints, checks, and what the code does and does not do yet. Where a workflow uses a ring from the profiles repository, it says which of the ring’s parts are implemented and which are deferred.
- The ring inside your process: the shared-memory ring and the envelope store without the daemon, which is what the HFT ring builds on.
- HFT: one feed, many readers, no copy per reader: one writer, four readers and a ring in process memory, with the wait strategy chosen for the host and the path to several writers and NUMA placement.
- Robotics: a 4 MiB scan through shared memory: a scan in a shared-memory pool, a 16-byte reference in the Pulse, read and checked in another process, and the pool’s limits seen.
- Enterprise: Pulses between services over TLS 1.3: one frame across a verified TLS 1.3 handshake, and the stage’s own statement of which modes are deferred.
- Distributed compute: jobs across machines through linked daemons: two daemons linked over sealed UDP, a job crossing from one to a subscriber of the other, and the path to the swarm across hosts, LAN discovery and a relay.
The ring inside your process
Workflow · for a Rust developer who wants the shared-memory ring and the envelope store without the daemon; about fifteen minutes, with the standard alone
You will have: one program that creates a ring and a store in shared memory under a name, publishes a Pulse that points at an envelope, opens both again by name the way a second process would, and reads the Pulse and the envelope back, verified; the ring’s and the store’s own test suites run; and the edges seen rather than read about: a ring opened with the wrong capacity, a ring opened by name alone, two envelopes with the same content, a fetch of an envelope never stored, and the names gone once their creator drops.
You need: the standard’s repository set up as Start here describes, and the Rust toolchain, on Linux or macOS: the ring and the store are POSIX shared memory.
The daemon is a convenience on top of two primitives. The ring carries 128-byte Pulses through shared memory with no lock, and the store beside it holds payloads under their content identifiers. Both come from the core-ring and core-store crates, and any process can use them.
Hand this to your agent
Run the workflow "The ring inside your process" from book/src/workflows/ring-in-process.md in the pulse-mesh repository:
1. Run cargo run --release -p example-03-embed-ring-rust and show me its output. Expect one line beginning "OK — published one Pulse referencing envelope b6a5ee1e601cf94a3bd3ba244ddc0ee8".
2. Read examples/03-embed-ring-rust/src/main.rs and explain in plain words what crosses the ring and what sits in the store, and how the reader knows its copy of a slot is whole.
3. Beside the repositories, make a crate (cargo new ring-edges) with path dependencies core-ring, core-store and pulse-canon on ../pulse-mesh/core/core-ring, ../pulse-mesh/core/core-store and ../pulse-mesh/pulse/pulse-canon; save the edges program from the page as src/main.rs and run it. Expect: a capacity-mismatch error for the open with the wrong capacity, 64 from open_existing, "same id: true", "None" for the fetch, 0 publishes before any, and an shm_open error with errno 2 after the creator dropped.
4. Run cargo test -p core-ring --release and cargo test -p core-store --release in pulse-mesh and show me the result lines; they hold the ring and the store to the rules the specification states.
Stop and tell me if anything fails, and say which step.
Or do it by hand
1. Run the example
From the repository root:
cargo run --release -p example-03-embed-ring-rust
OK — published one Pulse referencing envelope b6a5ee1e601cf94a3bd3ba244ddc0ee8 and read both back through a second handle.
The envelope’s identifier is the same on every run and every machine, because it is a hash of the envelope’s content and the content is fixed.
2. Create the ring and the store, and publish
The program is examples/03-embed-ring-rust; these two listings are its main, apart from choosing a name and the final line.
#![allow(unused)]
fn main() {
// One writer creates the ring: a 64-byte header plus 64 slots of 128 bytes.
// Under a name that already holds a ring, `create` attaches to that ring
// as it stands instead.
let mut ring = PulseRing::create(&name, 64).expect("create ring");
let store = EnvelopeStore::create_or_open(&store_name, 16).expect("create store");
// The payload travels as an envelope, addressed by its content.
let envelope = PulseEnvelope::typed("event.fired", b"payload".to_vec());
store.store(&envelope).expect("store envelope");
// The Pulse is the signal: a Data instruction whose envelope_ref names the envelope.
let mut pulse = Pulse::zeroed();
pulse.instruction = Instruction::Data as u16;
pulse.monotonic_nanos = 1;
pulse.set_envelope_id(&envelope.id());
let write_number = ring.publish(&pulse);
assert_eq!(write_number, 1);
}
PulseRing::create makes a shared-memory object named /pms-example-<pid>, sizes it for a 64-byte header plus 64 slots of 128 bytes, and maps it; under a name that already holds a ring it attaches to that ring as it stands instead, and a handle that attached does not unlink the name when it drops. EnvelopeStore::create_or_open does the same for the store. store writes the envelope once, under its identifier. The Pulse is the signal: a Data instruction whose envelope reference set_envelope_id sets to the envelope’s identifier. publish marks the next slot’s counter odd, copies the Pulse in, marks the counter even, and advances the cursor; it returns 1 because this is the ring’s first publish.
3. Open both by name and read
#![allow(unused)]
fn main() {
// Another process opens both by name; the capacity must match the creator's.
let reader = PulseRing::open(&name, 64).expect("open ring");
let reader_store = EnvelopeStore::create_or_open(&store_name, 16).expect("open store");
// The newest slot is one behind the cursor. read_slot copies it and keeps
// the copy only if the slot's counter was even and unchanged across the copy.
let newest = (reader.cursor_position() - 1) % reader.capacity();
let seen = reader.read_slot(newest).expect("a consistent Pulse");
assert_eq!(seen.instruction, Instruction::Data as u16);
// The envelope comes back by identifier, from the store beside the ring.
let id = seen.envelope_id().expect("the signal names an envelope");
let found = reader_store.fetch(&id).expect("the referenced envelope");
assert!(found.verify(), "the identifier matches the content");
assert_eq!(found.type_name(), Some("event.fired"));
}
PulseRing::open attaches to the existing object, checks its size and then the header the creator wrote, and refuses a ring whose header records another capacity or slot size, so a mismatch is an error rather than a fault; PulseRing::open_existing opens a ring by name alone and takes the geometry from the header. The newest slot is one behind the cursor. read_slot copies it and keeps the copy only if the slot’s counter was even before the copy and unchanged after it; it retries up to a hundred times and returns None if the slot was never written or a writer is stuck on it. envelope_id reads the reference back as an identifier, fetch looks the envelope up by it, and verify recomputes the identifier from the content.
The creator unlinks the shared-memory names when it drops, so running the program twice leaves nothing behind.
4. See the edges
Beside the repositories, cargo new ring-edges, with these under [dependencies]:
core-ring = { path = "../pulse-mesh/core/core-ring" }
core-store = { path = "../pulse-mesh/core/core-store" }
pulse-canon = { path = "../pulse-mesh/pulse/pulse-canon" }
and this as src/main.rs:
use core_ring::PulseRing;
use core_store::EnvelopeStore;
use pulse_canon::PulseEnvelope;
fn main() {
let name = format!("/pms-edges-{}", std::process::id());
let ring = PulseRing::create(&name, 64).expect("create");
if let Err(e) = PulseRing::open(&name, 32) {
println!("open with the wrong capacity: {e}");
}
let by_header = PulseRing::open_existing(&name).expect("open_existing");
println!("open_existing took the capacity from the header: {}", by_header.capacity());
let store = EnvelopeStore::create_or_open(&format!("{name}-env"), 16).expect("store");
let a = PulseEnvelope::typed("event.fired", b"payload".to_vec());
let b = PulseEnvelope::typed("event.fired", b"payload".to_vec());
println!("two envelopes with the same content, same id: {}", a.id() == b.id());
store.store(&a).expect("store a");
let c = PulseEnvelope::typed("event.fired", b"payloaD".to_vec());
println!("fetch of an envelope never stored: {:?}", store.fetch(&c.id()).map(|e| e.id()));
println!("reader sees {} publish(es) before any", ring.cursor_position());
drop(ring);
if let Err(e) = PulseRing::open_existing(&name) {
println!("open after the creator dropped: {e}");
}
}
cargo run --release
open with the wrong capacity: ring header capacity mismatch: the region has 64, the caller expects 32
open_existing took the capacity from the header: 64
two envelopes with the same content, same id: true
fetch of an envelope never stored: None
reader sees 0 publish(es) before any
open after the creator dropped: shm_open failed: errno 2
5. Run the rules under test
cargo test -p core-ring --release
cargo test -p core-store --release
Every test result line reads ok. The ring’s suites include the torn-read test: one writer as fast as it can against four readers, counting reads assembled from two publishes, and the count is zero.
Check it worked
| Check | Command | Pass |
|---|---|---|
| A Pulse and its envelope crossed to a second handle and verified | cargo run --release -p example-03-embed-ring-rust | The OK — line with b6a5ee1e601cf94a3bd3ba244ddc0ee8 |
| A mismatched open is an error, not a fault | The edges program | ring header capacity mismatch: the region has 64, the caller expects 32, then 64 from open_existing |
| Identity is content | The edges program | same id: true; the never-stored envelope fetches None |
| The creator owns the names | The edges program | shm_open failed: errno 2 after drop(ring) |
| The guarantees hold | cargo test -p core-ring --release, cargo test -p core-store --release | Every result line ok |
If create fails with RegionTooSmall or LayoutMismatch, the name holds something other than a ring of that capacity, likely left by a program that did not exit cleanly; create never resizes or reformats an object, so pick another name or remove it. A leftover ring of the same capacity is attached to as it stands, old slots and cursor included, and a handle that attached does not unlink it. On Linux, NoSpace at creation means /dev/shm cannot hold the ring or the store; the pages are reserved when the object is made, so this is an error then rather than a fault at the first touch.
What happened
- The ring is the signal, the store is the content. A 128-byte Pulse carries the envelope’s 16-byte identifier; the envelope sits in the store under that identifier, written once. Any process that maps both by name reads the Pulse, fetches the envelope, and verifies it by recomputing the identifier from the content.
- A reader never sees a torn Pulse. Each slot carries a counter that is odd while a write is in progress and even when the slot is stable. A reader copies the slot and keeps the copy only if the counter was even before and unchanged after; otherwise it retries, up to a hundred times, and reports nothing for a slot whose writer died mid-write. Readers never write, so the writer never waits for a reader. The SeqLock ring states the protocol; The SeqLock memory model says why it holds.
- Geometry is checked, not assumed. The ring’s header records its capacity and slot size, and
openrefuses a header that disagrees with what the caller expects;open_existingtakes the geometry from the header and needs no capacity. - The creator owns the names. A handle that created an object unlinks its name when it drops; a handle that attached does not. Running a program twice leaves nothing behind, and a second process must open the names while the creator lives.
- Identity is content. Two envelopes with the same type name and payload have the same identifier, so the store holds one copy per distinct envelope, and a fetch by an identifier nothing was stored under is
None.
What these two do not do: the ring overwrites the oldest slot when it is full, so a reader more than a capacity behind loses publishes; one that walks positions with read_position is told which, as lost (the ring chapter, readers that fall behind); the store is shared memory and vanishes with its creator, so it is not persistence (the daemon’s basket is the record); and neither routes, so two programs that share a ring agree on its name out of band, which is what the daemon does for them.
Make it yours
- Two real processes. One program creates the ring and the store and publishes; another opens them by name with
PulseRing::open_existingandEnvelopeStore::open_existing(oropen(name, capacity)) and reads; a reader must notcreate_or_openthe store, or one that starts after the creator has gone makes a fresh, empty store instead of failing. Keep the creator running while the reader opens. - Walk every publish.
PulseRing::read_position(position)returns each position’s Pulse once, as itself, reports a position the writer has not reached as not written, and reports a lapped position as lost; it is what the daemon’s runtime polls with. - Many readers, no copy per reader. HFT: one feed, many readers does this in process memory; across processes,
PulseRing::create_with_readers(name, capacity, max_readers)gives each reader a cursor andpublish_blockingkeeps the slowest registered reader’s pace. - Payloads too large for a store slot. Robotics: a 4 MiB scan through shared memory puts the payload in a pool and a 16-byte reference in the Pulse.
Next
- The SeqLock ring and The envelope store: the chapters these crates are held to.
- Why a 128-byte atom and Why content addressing: the two decisions underneath.
- Two programs talk through the daemon: the same two primitives behind a socket, with routing.
Spoken walkthrough
A script for the video and audio version of this page, about ninety seconds.
Take the daemon away, and two things are left: a ring and a store, both in shared memory, both opened by name.
The program creates them, writes an envelope into the store, and publishes one Pulse into the ring. The Pulse is a hundred and twenty-eight bytes, and sixteen of them are the envelope’s identifier, a hash of its content.
Then it opens both again by name, the way a second process would, reads the newest slot, fetches the envelope by the identifier the Pulse carries, and verifies it by recomputing the hash. One line: OK.
How does the reader know its copy of the slot is whole? Each slot has a counter. Odd while a write is happening, even when it is done. The reader copies, checks the counter was even before and unchanged after, and otherwise copies again. The writer never waits for anyone.
The second program walks the edges. Open the ring with the wrong capacity, and it tells you what the header says. Open it by name alone, and it takes the geometry from the header. Make two envelopes with the same content, and they have the same identifier. Ask for one you never stored, and you get none. Drop the creator, and the names are gone. Everything the daemon does for you, you have just done by hand.
Get one task done
Get one task done
Steps in order, with the result you should see after each one.
Part opener · for people who already run Pulse Mesh
A guide does not teach. It names the task, states what you need, and gives the steps in order, with the result you should see after each one. Background belongs in the Concepts part and exact facts in the Reference part; a guide links to both instead of repeating them.
- Tune the UDP transport: backpressure, retransmission and parity, and their counters.
- Write a language binding: the vectors, chapter 3, and how the tree proves conformance.
- Run the smoke bench and read the result: the one command, the count to check first, and what each field means.
The guides for running the daemon (installing pms, running the daemon as a service, capturing envelopes, the MCP server, shared memory, troubleshooting, adding a stage to a ring) are in the daemon’s book.
Tune the UDP transport
How-to guide · for anyone running `pulse-wire-udp` over a link that loses packets, a receiver that falls behind, or a path where a round trip is too slow
The transport’s three mechanisms are independent and default to off. Switch on the one whose symptom you have, then read the counters.
The receiver falls behind
Enable backpressure on both ends. The receiver acknowledges its cursor, one past the highest sequence it has seen, every millisecond, and the sender stalls when more than sixteen frames are in flight beyond it.
#![allow(unused)]
fn main() {
use pulse_wire_udp::{BackpressureConfig, Receiver, Sender};
let mut rx = Receiver::bind("127.0.0.1:9000")?;
let mut tx = Sender::bind("127.0.0.1:0")?;
tx.connect(rx.local_addr()?)?;
// The sender's feedback socket and the receiver's ACK target are one endpoint.
let any = "127.0.0.1:0".parse()?;
let mut cfg = BackpressureConfig::new(any, any);
tx.enable_backpressure(cfg.clone())?;
cfg.ack_target_addr = tx.ack_local_addr().expect("backpressure is on");
rx.enable_backpressure(cfg)?;
}
The first address in new is where that side binds its feedback socket and the second is where it sends feedback; the sender only receives, so it reports the address it bound with ack_local_addr and the receiver targets that. target_queue_depth (16) and ack_period (1 ms) are fields on the config; raise the depth for a link with a long round trip, and never set it to zero, which enable_backpressure refuses because it would admit no frame. A batch (send_batch) is held to the same rule frame by frame and stops at the first frame that stalls, with the frames before it sent. A batch receive (recv_batch) acknowledges and tracks gaps per datagram exactly as the single receive does, so a sender under backpressure is not stalled by a receiver that batches. A stall lasts at most stall_timeout, five seconds by default (with_stall_timeout; None removes the deadline), after which the send fails with SenderError::Stalled and the frame is not sent, so a dead receiver cannot wedge a sender. Watch tx.observed_recv_cursor() move; if it never does, the feedback address is wrong.
Frames are lost and the round trip is short
Add retransmission on top of backpressure with with_nack(capacity) on both configs. The sender keeps the last capacity encoded frames (1,024 by default) and the receiver asks for the ones it misses: each missing sequence is staged once, requested at most sixteen per period, requested again after four periods if it is still missing, and given up after three requests.
#![allow(unused)]
fn main() {
let cfg = BackpressureConfig::new(bind, target).with_nack(1024);
}
A retransmitted frame can arrive after its late original, so a caller that must see each sequence once filters on RecvMeta::seq. Read tx.retransmit_count(), tx.cache_miss_count() (a miss means the receiver asked for a frame older than the cache; raise the capacity), rx.nacks_sent(), rx.nack_drop_count() (sequences given up on without a request: a gap wider than the cache, or the frames before a late joiner’s first) and rx.gap_resyncs() (a resync means a gap wider than the cache; the stream skipped ahead rather than stall).
Frames are lost and the round trip is long
Enable parity. Every window of K frames gets one parity frame, and the receiver rebuilds one loss per window without asking.
#![allow(unused)]
fn main() {
tx.enable_fec(16); // window of 16 frames
rx.enable_fec(16, 64); // the same window, up to 64 windows in flight
}
A smaller window recovers more often and costs more bandwidth. rx.fec_recovered_count() says how often it paid off; two losses in one window fall through to retransmission if that is on. A frame rebuilt from parity can be followed by its late original, so the same filter on RecvMeta::seq applies.
The feedback socket must not listen to strangers
#![allow(unused)]
fn main() {
let cfg = BackpressureConfig::new(bind, target).with_feedback_peer("10.0.0.7".parse()?);
}
Feedback from any other address is dropped and counted in tx.feedback_rejected_count(). A count that climbs while the cursor stalls means the filter names the wrong peer: a multi-homed receiver may answer from another address than the one you send to.
Do not tune
The envelope size is not a knob: a datagram carries at most 1,315 envelope bytes and the transport does not fragment. An envelope larger than that belongs in the envelope store with a Pulse referencing it, or on a transport with a larger unit. The transport chapter has every constant; Transport design says why each mechanism is shaped as it is.
Write a language binding
How-to guide · for anyone implementing the codec or a client in another language and wanting it to conform
A binding conforms when it reproduces the vectors byte for byte. There are two kinds of binding in this tree and a third kind outside it, and the guide covers all three.
Know which kind you are writing
- A codec binding wraps
pulse-canonfor another runtime. The Python, Node and C bindings underperipheral/bindings/do this with native extensions, and the WebAssembly binding does it for the browser. They inherit conformance from the codec; their tests prove the wrapper adds nothing. - A thin client speaks the port protocol to a running daemon over a WebSocket, without the codec. The Python and Node thin clients in the daemon repository (
clients/) are the pattern: attach, send, on, tap, run, detach, in about two hundred lines. - An independent implementation of the codec in another language is a satellite repository: it proves conformance against the vectors on its own and registers in the profiles repository’s
modules.toml, which lists none today.
1. Start from the vectors
The three files under pulse/pulse-spec/test-vectors/ are the contract: pulse-layout-known-good, envelope-raw-known-good and envelope-nested-3-level, each as a JSON description of the fields and as the exact hex bytes. Your first test loads a vector, builds the value from the JSON fields, encodes it, and compares the bytes; your second decodes the hex and compares the fields; your third recomputes the identifier and compares it with the one in the vector.
2. Implement chapter 3 exactly
The specification’s PulseEnvelope chapter is the whole codec: the 44-byte header, the type-name section with its tag and length, the payload section with its variant tag, and the identifier preimage with the domain tag and the length prefixes. Two things catch most implementations: the type-name length is a byte length of UTF-8, not a character count; and the identifier’s preimage begins with the domain tag pulse-envelope-id/v2 and includes the payload’s byte length before its bytes. If the raw vector’s identifier matches, the nested one usually does; if it does not, the nested payload’s count prefix is the place to look.
3. Prove it the way the tree does
In the browser, the WebAssembly binding’s check_fixture runs every vector through the compiled codec. For a native binding, mirror bindings/wasm/tests/smoke.mjs: round-trip each vector, then build a typed envelope, tamper with one payload byte and confirm verify fails. A thin client has nothing to prove against the vectors; its test is a session against an embedded daemon, which mesh-hub’s embed module provides in a few lines.
4. Register a satellite
An implementation outside this repository adds an entry to modules.toml, in the profiles repository, naming its repository, its tier, its conformance surface and its registry status; the file defines those fields and holds no entry yet. The entry is how the registry finds it; the vectors are how it is judged.
Run the smoke bench and read the result
How-to guide · for anyone who wants a measurement from this machine and needs to know what it is a measurement of
What you need
- A checkout of this repository and the Rust toolchain it pins in
rust-toolchain.toml. - Nothing running. The bench needs no daemon and no shared memory.
Run the bench
cargo run --release -p pulse-cli -- bench --count 200000 --json
It sends 200,000 Pulses, each with a 64-byte envelope, from one socket to another over the loopback interface through the reference UDP transport, and prints one JSON object with the fields below. Without --count it sends 10,000.
Always run it in the release profile. A debug build measures the compiler, not the transport. Run it more than once, because the tail moves with whatever else the host is doing.
pms bench smoke runs the same bench and prints the same fields; its flags are in The pms command line.
Check the count before anything else
Compare pulses_received with pulses_sent. In a valid run they are equal.
The bench sends as fast as one thread can, with the transport’s backpressure off. When the receiving socket’s buffer fills, the kernel drops datagrams, and the receiver stops only after five seconds pass without one. A run where pulses_received is less than pulses_sent is not a result. Its percentiles describe only the Pulses that arrived. The command still exits 0. Discard the run and repeat it. A shortfall that repeats means the receiver cannot keep pace with an unpaced sender at that count on this host.
Read the fields
| Field | Meaning |
|---|---|
pulses_sent | Pulses the sender sent. |
pulses_received | Pulses the receiver decoded. |
payload_bytes | The envelope size of every Pulse, 64. |
send_elapsed_ns | Wall time the sender spent sending, in nanoseconds. |
recv_elapsed_ns | Nanoseconds from the receiver’s first read to its last decode. The timeout that ends a short run is not counted. |
throughput_pulses_per_sec | pulses_received per second of recv_elapsed_ns. |
p50_ns, p99_ns, p999_ns | One-way latency at the median, the 99th and the 99.9th percentile, in nanoseconds: the receiver’s clock at decode minus the sender’s clock carried in the frame. Both are the host’s monotonic clock, so the two readings compare on one host and on no other. |
What the latency includes
The sender is not paced. When it sends faster than the receiver decodes, Pulses wait in the receiving socket’s buffer, and that wait is part of their latency. While the sender stays ahead, the queue grows with every Pulse, up to what the receiving socket’s buffer holds, so the latencies depend on the count and on how far the sender gets ahead. A run that would queue more than the buffer holds drops Pulses and is not a result. The percentiles describe a burst through two sockets, not one Pulse crossing an idle path. Compare two runs by throughput, and only at the same count. Beyond the wait, the one-way latency through two sockets is mostly the loopback path’s price: encoding and decoding a Pulse is a small part of it.
What is not here
The smoke bench measures Pulse Mesh alone, over UDP loopback. It says nothing about the shared-memory ring, the HFT ring’s wait strategies and NUMA placement, or a path between two hosts.
What is exactly true
What is exactly true
The specification, the workspace crate by crate, and the tables generated from the code.
Part opener · for implementers and integrators looking something up
Reference pages state what is true and nothing else. They do not teach and they do not persuade. Where a table can be produced from the code, it is, and a check in continuous integration fails when the page and the code disagree.
The part has four layers. The specification is the normative text: the Pulse, the PulseEnvelope, the ring, the store, the UDP transport, conformance, and what changes require a draft revision; the vectors under pulse/pulse-spec/test-vectors are its executable form. The workspace pages describe each tier and each crate, following the tree’s own structure. The API pages describe the surfaces other languages see. The generated pages are produced from the code by the gate.
- The specification: seven chapters here and an appendix; chapter 7, the port protocol, is in the daemon’s book, since the daemon speaks it.
- Workspace: Layout, pulse/, core/. The daemon’s tier and the rings have their pages in the daemon’s book and the profiles’ book.
- Generated from the code: the crate index and the error catalogue; the gate regenerates them and fails on any difference.
- API: Overview, Python, Node, C, and the language bindings by kind.
- Glossary.
The specification
The specification
The normative description of the Pulse, the PulseEnvelope, the ring, the store, the UDP transport and the port protocol, and what a conformant implementation must reproduce.
Reference · normative · for anyone implementing, verifying or extending Pulse Mesh in any language
This is the Pulse Mesh Standard as the reference implementation in this repository realises it. Where a chapter describes the reference implementation rather than a requirement on every implementation, it says so at the top.
Status. Working draft, lineage draft-00. Breaking changes between draft revisions are expected and are recorded, not hidden: a superseded rule stays in the text, marked as superseded, with the revision that replaced it. The conformance vectors under pulse/pulse-spec/test-vectors are the executable form of chapters 2 and 3; the chapter text and the vectors are checked against the code by the documentation gate and the workspace tests.
How to read the requirement words. MUST, MUST NOT, SHOULD, SHOULD NOT and MAY carry their usual normative meaning. A statement without one of those words is a description of the reference implementation, not a requirement on others.
| Chapter | Subject | Normative for |
|---|---|---|
| 1. Introduction | Scope and terminology | Everyone |
| 2. The Pulse | The 128-byte atom: layout, alignment, instructions, the heartbeat overlay | Every implementation |
| 3. The PulseEnvelope | The canonical bytes, the content id, payload variants, recursion, self-verification | Every implementation |
| 4. The SeqLock ring | The shared-memory ring: header, slots, writer and reader protocol, crashed writers | Shared-memory substrates |
| 5. The envelope store | The content-addressed store beside the ring: layout v2, claims that route around held slots, verified reads, lapping | The reference implementation |
| 6. The UDP transport | Datagram framing, CRC, feedback, FEC, the capability handshake | The reference transport |
| 7. The Universal Port Protocol | The daemon’s JSON protocol over WebSocket and Unix socket, and its connection policy | Every client of the daemon |
| 8. Conformance | What a conformant implementation reproduces, and what changes require a draft revision | Everyone |
| Appendix A. BLAKE3 | The hash the content id is built on | Informative |
1. Introduction
Reference · normative · scope and terminology
Pulse Mesh is a wire format and a shared-memory transport for high-throughput, low-latency message passing. It is built on two primitives, both specified normatively in this standard.
- The Pulse: a 128-byte, fixed-size, cache-aligned control and data signal, exchanged through a shared-memory ring or a UDP datagram.
- The PulseEnvelope: a variable-size, content-addressed, byte-stable payload, referenced from a Pulse when data exceeds the Pulse boundary.
The Pulse is the core primitive. The PulseEnvelope is the standard primitive for variable-size payloads. Chapters 2 and 3 are the load-bearing standardisation claims; chapters 4 to 7 describe the substrates and the protocol the primitives flow through; chapter 8 defines conformance.
1.1 Scope
This standard defines the byte layout, alignment and field semantics of the Pulse (chapter 2); the canonical byte layout, the content-addressing scheme, the payload variants and the self-verification invariant of the PulseEnvelope (chapter 3); the SeqLock-protected shared-memory ring protocol Pulses are exchanged through (chapter 4); the envelope store that holds envelopes referenced from Pulses (chapter 5); the reference UDP transport’s framing, feedback, error correction and capability handshake (chapter 6); the Universal Port Protocol the daemon speaks to its clients (chapter 7); and the conformance requirements and the normative surface (chapter 8).
It does not define higher-layer routing, governance, identity or scheduling semantics, which build on Pulse Mesh and are specified elsewhere in the Sovereignty stack; production hardening such as failover, persistence, replication or consensus, since the reference implementation is reference-grade; or language bindings, optimised variants and alternative transports, which live in satellite repositories that conform through the vectors in chapter 8 and register in the profiles repository’s modules.toml.
1.2 Terminology
Requirement words. MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be read as in RFC 2119.
Conformance terms. A conformant implementation produces byte-identical output to the vectors under pulse/pulse-spec/test-vectors for the field inputs they define, and accepts byte-identical input that any other conformant implementation produces. Conformance is byte equality, not API parity.
Surface terms. The wire format is the byte representation of a Pulse or an envelope. The canonical encoding is the specific byte sequence a PulseEnvelope produces under section 3.1. A substrate is the medium a Pulse or an envelope is exchanged through: a shared-memory ring, a UDP datagram, a daemon connection.
Names. The glossary owns every other term this standard uses, in the sense this standard uses it.
2. The Pulse
Reference · normative · the 128-byte atom every conformant transport carries or references
The Pulse is the core primitive. Every message exchanged through a conformant transport is a Pulse or references one. The reference type is pulse_canon::Pulse; the vector pulse-layout-known-good under pulse/pulse-spec/test-vectors is its executable form.
2.1 Wire layout
A Pulse MUST be exactly 128 bytes. The layout is C-compatible field order with no padding beyond the declared fields, and the type is aligned to a 64-byte boundary in memory (#[repr(C, align(64))] in the reference). All multi-byte integers are little-endian on the wire. The reference keeps a Pulse in memory as exactly these bytes, so it builds only for little-endian targets.
| Offset | Size | Field | Type | Semantics |
|---|---|---|---|---|
| 0 | 8 | seq_lock | u64, atomic | The SeqLock word of the slot the Pulse sits in: odd while a writer is in progress, even when stable. Owned by the substrate (chapter 4); a Pulse on the wire carries whatever value the sender’s slot had, and receivers MUST ignore it. |
| 8 | 16 | origin_id | [u8; 16] | The producing node’s identifier: a truncated hash of the node’s name or key. A hub writes its own instance identifier, sixteen random bytes drawn when it was created or opened, into an origin_id left unset on the Pulses it publishes, and uses it to skip the envelope signals it wrote itself. |
| 24 | 8 | monotonic_nanos | u64 | A monotonic clock reading at creation, set by the producer, in nanoseconds. A hub stamps the signals it writes and the envelopes it routes with the host’s monotonic clock, the clock this field is defined on: CLOCK_MONOTONIC on Linux and CLOCK_UPTIME_RAW on macOS, both in nanosecond steps and neither counting time asleep. An application may drive the hub’s clock instead with set_monotonic_nanos. |
| 32 | 8 | chain_step | u64 | Remaining steps in the optional hash chain; a countdown to zero is a time to live. Zero when the replay defence is not used. |
| 40 | 2 | instruction | u16 | The opcode, from the table below. |
| 42 | 2 | _pad0 | u16 | Alignment padding. MUST be zero. |
| 44 | 4 | scope | u32 | The execution scope bound; interpreted as a context id and a subscope by the context registry. |
| 48 | 16 | envelope_ref | [u8; 16] | The content id of an associated PulseEnvelope, or all zeros when none is attached. |
| 64 | 32 | preimage | [u8; 32] | The hash-chain preimage for the optional replay defence. MAY be zero. |
| 96 | 32 | _pad2 | [u8; 32] | Fills the second cache line. Carries the heartbeat overlay (2.5) when the instruction is Heartbeat, and the data overlay (2.6) when it is Data. |
The first 64 bytes are one cache line and the second 64 bytes another; a Pulse occupies exactly two.
Compile-time invariant. A conformant implementation MUST assert at build time that its Pulse type is exactly 128 bytes and 64-byte aligned. The reference does so with a constant assertion in pulse/pulse-canon/src/pulse.rs, and PULSE_WIRE_SIZE is published as a public constant.
2.2 Instructions
| Opcode | Name | Meaning |
|---|---|---|
0x0000 | Null | A blank slot: zeroed memory or an empty buffer. |
0x0001 | Heartbeat | A liveness signal. Bytes 96 to 128 carry the heartbeat overlay (2.5). |
0x0010 | Data | A data signal. envelope_ref names the PulseEnvelope that carries the payload, and bytes 96 to 128 carry the data overlay (2.6). |
Receivers MUST treat an unknown opcode as Null and MUST NOT fail-stop on one; the reference’s Instruction::from_u16 maps every other value to Null. Future draft revisions MAY assign further opcodes.
2.3 Alignment
The Pulse type MUST be aligned to a 64-byte boundary, one cache line on contemporary x86-64 and AArch64 processors. The alignment prevents false sharing when adjacent slots are touched by different cores, makes a read a single cache-line fetch per line, and permits zero-copy reinterpretation of an aligned byte slice as a Pulse. Implementations on platforms with a different cache-line size MUST still align to 64 bytes for wire compatibility.
2.4 SeqLock access and multi-slot atomicity
When Pulses are exchanged through a shared-memory ring, the seq_lock word gives readers torn-read detection without a mutex; the protocol is chapter 4. The layout provides atomicity for one Pulse at a time. A producer that needs several Pulses to be observed together MAY wrap them as inner envelopes of one nested PulseEnvelope (3.4), identified by a single content id, and publish one Pulse that references it.
2.5 The heartbeat overlay
When instruction is Heartbeat, bytes 96 to 128 MUST be read as a HeartbeatPayload:
Offset within _pad2 | Size | Field | Type | Meaning |
|---|---|---|---|---|
| 0 | 8 | schema_version | u64 | An opaque schema or configuration version, compared bytewise. |
| 8 | 8 | uptime_ms | u64 | Milliseconds since the node started. |
| 16 | 4 | error_count | u32 | Errors since the node started. |
| 20 | 2 | stage_count | u16 | Registered processing stages. |
| 22 | 2 | path_count | u16 | Registered processing paths. |
| 24 | 2 | consecutive_errors | u16 | Errors in the most recent window. |
| 26 | 1 | wake_state | u8 | Position in the wake cycle, 0 to 15. |
| 27 | 1 | health_state | u8 | 0 healthy, 1 warning, 2 restarting, 3 isolated. |
| 28 | 4 | _reserved | [u8; 4] | MUST be zero. |
The reference reads and writes the overlay through heartbeat_payload and set_heartbeat_payload. When the instruction is neither Heartbeat nor Data, bytes 96 to 128 MAY hold any pattern and receivers MUST ignore them.
2.6 The data overlay
When instruction is Data, bytes 96 to 128 are a DataPayload, which names the stored copy of the envelope that envelope_ref identifies. Envelopes with the same content share an id, so the id alone cannot say which copy an envelope store (chapter 5) holds; the overlay names one by its store cursor.
Offset within _pad2 | Size | Field | Type | Meaning |
|---|---|---|---|---|
| 0 | 8 | store_cursor | u64 | The cursor at which the envelope store holds the copy, when has_store_cursor is 1. |
| 8 | 1 | has_store_cursor | u8 | 1 when store_cursor names a copy; any other value names none. |
| 9 | 23 | reserved | [u8; 23] | MUST be zero. |
All zeros, the _pad2 of a zeroed Pulse, name no copy. A receiver that reads envelopes from a store MAY fetch the copy at the named cursor, and MUST treat what it finds there as the signalled envelope only when it carries the id envelope_ref names, since the store may have reused the slot. A receiver that does not read a store MAY ignore the overlay. The reference writes it through set_data_payload and set_store_cursor, reads it through data_payload and store_cursor, and its hub fetches the named copy with EnvelopeStore::fetch_at, which checks the id.
2.7 What would refute this chapter
The vector pulse-layout-known-good.hex is the bytes of a Pulse with known field values. An implementation whose Pulse does not reproduce them field for field, or whose type is not 128 bytes and 64-byte aligned, does not conform; a reference build in which the layout assertion fails contradicts this chapter and is a defect.
3. The PulseEnvelope
Reference · normative · the variable-size, content-addressed payload and the cross-language byte-equality contract
The PulseEnvelope™ carries anything larger than a Pulse. It is referenced from a Pulse whose instruction is Data through the 16-byte envelope_ref, and it is addressed by its content: two envelopes with the same content have the same identifier in every language that implements this chapter. The reference type is pulse_canon::PulseEnvelope; the vectors envelope-raw-known-good and envelope-nested-3-level under pulse/pulse-spec/test-vectors are its executable form.
3.1 Canonical byte layout
The canonical bytes of an envelope are a fixed header, a type-name section and a payload section, in that order, with no padding. All multi-byte integers are little-endian. The reference publishes CANONICAL_FIXED_HEADER = 44 and MIN_CANONICAL_ENVELOPE = 50 (the smallest well-formed envelope: the header, an absent type name, a variant tag and a zero length) as public constants.
Fixed header, 44 bytes
| Offset | Size | Field | Type | Notes |
|---|---|---|---|---|
| 0 | 16 | id | [u8; 16] | The content identifier (3.2). |
| 16 | 16 | origin | [u8; 16] | The producing node. Not part of the identifier, so the same content from two origins shares one id. All zeros when unset. |
| 32 | 8 | monotonic_nanos | u64 | The producer’s creation time. Not part of the identifier. |
| 40 | 2 | envelope_type | u16 | The numeric type. Part of the identifier. |
| 42 | 2 | flags | u16 | Routing and behaviour flags (below). Not part of the identifier. |
Type-name section
| Offset | Size | Field | Type | Notes |
|---|---|---|---|---|
| 44 | 1 | type_name_present | u8 | 0 absent, 1 present. Any other value MUST be rejected (the reference reports InvalidTypeNameTag). |
| 45 | 4 | type_name_len | u32 | Present only when the tag is 1: the byte length L of the name. |
| 49 | L | type_name | [u8; L] | The UTF-8 bytes of the name. A decoder MUST validate UTF-8 (InvalidTypeName). |
Payload section, starting at T = 45 when the name is absent and T = 49 + L when present
| Offset | Size | Field | Type | Notes |
|---|---|---|---|---|
| T | 1 | payload_variant | u8 | 0 raw, 1 nested. Any other value MUST be rejected (InvalidPayloadVariantTag). |
| T + 1 | 4 | payload_len or nested_count | u32 | The byte length M of a raw payload, or the number N of nested envelopes. |
| T + 5 | M or … | payload | bytes | M opaque bytes, or N envelopes each encoded by this same rule, concatenated. |
Flags at offset 42:
| Bit | Mask | Name | Meaning |
|---|---|---|---|
| 0 | 0x0001 | BROADCAST | Fan out to every listener rather than point to point. |
| 1 | 0x0002 | ACK_REQUIRED | The sender expects an acknowledgement. |
| 2 | 0x0004 | PRIORITY | Schedule with elevated priority. |
| 3 | 0x0008 | COMPRESSED | The payload is compressed; the codec is outside this layer. |
| 4 to 15 | reserved | Encoders MUST NOT set them. The reference decoder drops unknown bits on decode; since flags are outside the identifier, an envelope that arrived with a reserved bit set decodes with it cleared and its id is unaffected. |
3.2 The content identifier
The identifier is the first 16 bytes of a BLAKE3 hash of this preimage, in this order:
- the domain tag, the ASCII bytes of
pulse-envelope-id/v2, with no terminator; envelope_typeas two little-endian bytes;- the type-name section exactly as it appears on the wire: the byte
0, or the byte1followed by the four-byte length and the name bytes; - the payload variant tag as one byte;
- the length of the payload bytes as four little-endian bytes;
- the payload bytes: a raw payload verbatim, or for a nested payload the count as four little-endian bytes followed by each child’s canonical bytes.
The domain tag keeps identifiers computed under this rule distinct from any other hash of the same fields, and the explicit lengths keep a name and a payload from being read as one another. origin, monotonic_nanos, flags and the identifier field itself are outside the preimage. The reference computes it with PulseEnvelopeId::compute_from_payload and renders it as 32 lowercase hexadecimal characters; the all-zero identifier means “no envelope” in a Pulse’s envelope_ref.
3.3 Payload variants
A raw payload (0) is a sequence of opaque bytes. Receivers MUST NOT interpret them at this layer; their meaning is carried by envelope_type or type_name.
A nested payload (1) is a sequence of envelopes, each in its canonical form. Its byte form is the count followed by the children, which is also how it enters the identifier’s preimage.
A decoder that meets any other variant tag MUST reject the envelope. Future draft revisions MAY define further variants.
3.4 Recursive composition
An outer envelope may contain inner envelopes, and those may contain envelopes in turn. Because a parent’s identifier covers its children’s canonical bytes, and each child’s canonical bytes begin with the child’s own identifier, tampering with any descendant changes every identifier above it. Three uses follow: a batch of K events travels as one envelope with one identifier and is verified with one recomputation; an audit chain nests the previous state in the next so provenance is tamper-evident inline; a broadcast of a nested batch fans out K events to N subscribers without per-event framing.
Bounds. The reference decoder refuses nesting deeper than MAX_NESTING_DEPTH = 32 (NestingTooDeep) and refuses a nested count larger than the remaining bytes could hold at fifty bytes per child (NestedCountExceedsBuffer) before it allocates anything. A conformant decoder MUST accept nesting to a depth of 32 and MAY reject deeper structures; an encoder SHOULD NOT produce them. The vector envelope-nested-3-level pins three levels.
Superseded rule. Draft-00 required decoders to handle recursion “to arbitrary depth bounded only by available memory”. That rule is superseded by the depth and count bounds above, adopted after the decoder was found to allocate from an attacker-chosen count.
3.5 Self-verification
A receiver verifies an envelope by recomputing the identifier from envelope_type, type_name and the payload and comparing it with the stored id; the reference exposes this as PulseEnvelope::verify(). Decoding checks structure only, so verification is a separate step; the reference UDP transport performs it on every decoded envelope and rejects a mismatch (EnvelopeIdMismatch). Receivers SHOULD verify envelopes that arrive over an untrusted transport and MAY skip verification on a trusted one, such as local shared memory.
3.6 Byte stability across languages
Two conformant implementations in different languages MUST produce byte-identical canonical bytes, and therefore identical identifiers, for identical field inputs. The reference’s canonical-layout tests are the executable contract in Rust; the same fixture appears as envelope-raw-known-good for language-neutral conformance; the Python, Node, C and WebAssembly bindings in this repository, and the satellite implementations, prove equality against it.
3.7 Size is unbounded
The canonical envelope has no maximum size. A conformant implementation MUST accept any byte count subject only to its memory. Three limits exist at other layers, and none of them is part of the envelope contract:
| Limit | What it is | Where it lives |
|---|---|---|
| 1,315 bytes | The most envelope bytes one datagram of the reference UDP transport carries: the 1,472-byte MTU less the 153-byte frame header and the 4-byte CRC trailer (chapter 6). | pulse-wire-udp, MAX_ENVELOPE |
| 65,536 bytes | The most bytes one entry of the reference envelope store holds; recorded in the store’s header so an attaching reader discovers it (chapter 5). | core-store, MAX_ENTRY_SIZE |
| The daemon’s frame cap | The largest message the daemon accepts on one connection (chapter 7). | mesh-hub |
A transport MAY refuse to carry an envelope its substrate cannot hold; that refusal is a substrate concern, not a violation of this chapter.
3.8 A Pulse, an envelope, or both
This section is informative. A Pulse alone is sufficient when the signal fits in its fields: Null, Heartbeat with the overlay, or Data with nothing attached. An envelope is required when the application carries variable-size data, needs an identity that survives transmission, needs to bundle several messages atomically, or needs cross-language byte equality. The common pattern is both: a Pulse with instruction Data and envelope_ref set to the envelope’s identifier, the Pulse carrying the transport-level signal and the envelope the payload.
3.9 What would refute this chapter
The two envelope vectors give the canonical bytes and the identifiers of a raw envelope and a three-level nested one. An implementation that produces different bytes or a different identifier for their inputs, accepts a tag outside 0 and 1, accepts an invalid type name, or computes an identifier without the domain tag and the lengths, does not conform.
4. The SeqLock ring
Reference · normative for shared-memory substrates · the ring Pulses are exchanged through on one machine, and the protocol that keeps a reader from ever using a torn Pulse
A PulseRing is a fixed-capacity ring of Pulse slots in POSIX shared memory, with one writer per slot at a time and any number of readers, protected slot by slot by a SeqLock: a sequence counter that is odd while a write is in progress and even when the slot is stable. The reference type is core_ring::PulseRing. This chapter is normative for any implementation that exchanges Pulses through shared memory; a different substrate MAY use a different protocol and conforms through the vectors of chapter 8.
4.1 Region layout
| Bytes | Contents |
|---|---|
| 0 to 8 | write_cursor, atomic u64: the number of publishes so far; the next publish lands in slot write_cursor mod capacity. |
| 8 to 16 | total_writes, atomic u64: the count readers poll for new data. |
| 16 to 24 | read_cursor, atomic u64: the slowest reader’s position, consulted only by the blocking publish mode. |
| 24 to 28 | The magic PMSR, stored last when the ring is formatted, with release ordering. |
| 28 to 32 | The header layout version, 1; it numbers the header, not the slot format. |
| 32 to 40 | The capacity in slots, u64. |
| 40 to 44 | The slot size in bytes: 128, or 192 for version-2 slots. |
| 44 to 64 | Reserved, zero; the header is one cache line. |
| 64 onward | capacity slots of one Pulse each. |
A version-1 slot is 128 bytes, the Pulse itself, whose first 8 bytes are the slot’s SeqLock counter. A version-2 slot is 192 bytes: the same 128-byte Pulse, then an 8-byte ready line at offset 128 that the writer bumps once after every publish, then padding to the next 64-byte boundary. The ready line exists so a receiver waiting in hardware (a monitor-and-wait instruction on the line) is woken by one write rather than by the two SeqLock stores; a reader MAY also poll it in place of the slot’s counter (4.3). The reference publishes HEADER_SIZE = 64, PULSE_SIZE = 128, PULSE_SIZE_V2 = 192 and READY_LINE_OFFSET = 128; a region MUST be HEADER_SIZE + capacity × slot size bytes. Whoever formats a region writes the version, the capacity and the slot size and then stores the magic with release ordering, so a reader whose acquire load sees the magic sees the rest. An implementation that opens a region MUST check its size and then its header before using it: the reference refuses a smaller object with RegionTooSmall and a header whose magic, version, capacity or slot size is not what it would map with LayoutMismatch, after waiting about fifty milliseconds on a zero magic for a creator that has sized the object but not yet published its header. open_existing takes the capacity and slot size from the header, so a reader need not know how the ring was made.
4.2 Writer protocol
To publish a Pulse into slot i, a writer MUST:
- read the slot’s counter and clear its low bit, so that a counter left odd by a writer that died is re-based rather than compounded;
- store the odd value (the even base plus one) with release ordering, marking the write in progress;
- issue a release fence, so the mark is visible before any payload byte;
- copy the Pulse’s bytes 8 to 128 into the slot, leaving the counter word alone;
- store the even value (the base plus two) with release ordering, marking the slot stable;
- on a version-2 slot, bump the ready line with release ordering.
The reference has seven publish paths (publish, publish_returning_slot, publish_at_slot, publish_batch, publish_blocking, publish_batch_blocking and publish_with_crc), and all seven write each slot by these steps; the counter is monotonic across them. Every path but publish_at_slot then advances total_writes and write_cursor, each with release ordering, so a reader whose acquire load sees either sees the slot stable. publish_at_slot overwrites a slot in place and moves neither, which is why the counter and ready-line readers of 4.3 exclude a ring it writes.
Crashed writers. A writer that dies between steps 2 and 5 leaves the counter odd. Readers give up on that slot (4.3) until the next publish into it, whose first step re-bases the counter, after which the slot reads normally. No reader ever accepts the torn contents.
Publish modes. In the default overwrite mode a publish always succeeds and a writer more than capacity ahead of a reader overwrites unread slots; the ring is lossy under sustained over-rate by design. In the blocking mode (publish_blocking) the writer spins while write_cursor − read_cursor would reach capacity, for workloads where every message matters.
4.3 Reader protocol
To read slot i, a reader MUST:
- load the counter with acquire ordering; if it is odd, a write is in progress: retry;
- copy the slot’s bytes out;
- issue an acquire fence, so the copy completes before the re-read;
- load the counter again; if it differs from the first load, the slot changed under the copy: retry;
- if the counter is zero, the slot has never been written: report nothing;
- otherwise the copy is a consistent Pulse.
The retry is bounded. The reference tries one hundred times and then reports nothing, which is the correct answer for a slot whose writer died; an implementation MUST bound its retries so a dead writer cannot hang a reader. Readers never write, so a writer never waits for a reader.
Readers that fall behind
A ring whose writer overwrites the oldest slot cannot promise a slow reader every publish, and it MUST NOT hand a reader the same publish twice or a newer lap’s Pulse under an older index. The reference’s readers that keep a cursor of their own, the SPMC ring and each multilane lane, apply one rule. Before a read, a cursor more than capacity behind the write cursor moves to write cursor − capacity, the oldest publish the ring still holds, and the publishes passed over are counted as lost. After the SeqLock copy, a copy the writer may have overwritten with the slot’s next lap, because the write cursor has passed the cursor plus the capacity or the counter shows a write newer than the reader’s lap, is discarded and counted; at the exact boundary the counter decides, and uncertainty is resolved as a counted loss. The SPMC ring reports a jump once as Lagged { skipped } and keeps a count per reader; a lane keeps a count per lane, and a round-robin poll that discarded a copy may return nothing for that pass, meaning retry. The hub runtime’s PulseHub::poll does not yet apply this rule and can re-deliver after being lapped. The hub runtime’s poll reads the same way through PulseRing::read_position, which the ring provides for any reader that walks the write positions in order: it returns each position’s Pulse once, as itself, reports a position the writer has not reached as not written, or reports it lost together with every position up to the next one to read, and never returns a later lap’s Pulse under an earlier position. The rule is the one above with the slot counter as the lap count: the counter starts at zero and each completed write adds two, so a stable copy whose counter shows more writes than the position’s lap needs may hold the next lap and is counted lost. The counter can run ahead of the laps, after a writer that died between its slot write and its cursor move, a batch cut short the same way, or a publish_at_slot, and never behind them under the single-writer contract, so every case the reader cannot settle is a counted loss and never a Pulse twice. A hub attaching to a ring that already holds Pulses starts at the oldest one still held, so its count of lost Pulses excludes the history from before it attached. Two variants serve a ring written only through its cursor (publish, publish_blocking and the batch forms), never publish_at_slot, and keep the same promise. read_position_by_counter trusts the slot’s counter alone: below the position’s lap count the position is not written, above it the position is lost, and equal to it a stable copy that still shows that count is the position’s Pulse. It loads the header’s write cursor only to place a loss, so a reader in step with the writer shares no cache line the writer updates per message. read_position_by_ready reads a version-2 ring the same way with the slot’s ready line as the lap count, one per completed write counted from one, so a reader waiting on it does not touch the slot’s two lines while the writer is storing them; the copy must still show the position’s counter, and on a version-1 ring it is read_position_by_counter. Either reports a loss with the oldest position the ring still holds, or the one after the lost position if that is later, as the next to read. Where publish_at_slot is in use, a counter run ahead could pass that slot’s Pulse off as a position not yet written, so such a ring MUST be read with read_position.
The fences in steps 3 of both protocols are what make the protocol correct on weakly ordered processors; the vector for this chapter is executable rather than static: the reference’s torn-read test drives four readers against one writer and asserts that no reader ever observes a Pulse with mixed bytes.
4.4 Integrity trailer
A writer MAY publish with a CRC32C trailer: the checksum of the Pulse’s bytes 8 to 124, stored little-endian in bytes 124 to 128 of the slot, which is the tail of the Pulse’s second padding field. A reader that verifies the trailer recomputes it over the same bytes and rejects a mismatch. The trailer sits at the same offset in version-1 and version-2 slots because both carry the Pulse in their first 128 bytes. pms-crc32c owns the computation.
4.5 Ownership and descriptors
A ring is created by name or opened by name (open). create_exclusive requires a free name and refuses one that holds an object. create and create_or_open are one call: a free name gets a new object, sized and formatted, that the handle owns and unlinks when it drops; a name that already holds a ring is attached as open attaches it, after the size and header checks, and is never resized, cleared or reformatted, so the Pulses a live ring holds survive the call on every platform, and a handle that attached does not unlink the name and reports is_creator false, which the hub runtime uses to make the creating hub the ring’s one writer. Concurrent callers are safe: one creates and the others attach, after waiting out, for about fifty milliseconds, a creator that has made the object but not yet sized it. A capacity of zero, or one whose ring would not fit a file offset, is refused (InvalidCapacity) before anything is created. Everyone closes the descriptor they opened. A ring MAY also be built over a descriptor the caller already holds (from_fd), for memory-file-backed or NUMA-placed rings; such a ring never unlinks a name, and it closes the descriptor only when the caller hands over ownership. from_fd formats a fresh, zeroed region with the header it needs, and checks the header of a populated one the way open does. A version-2 ring is made with create_exclusive_v2 or from_fd_v2; open refuses one, and open_existing attaches to either version. A new object is not zero-filled, since the kernel supplies zero pages; on Linux its pages are reserved with posix_fallocate when it is sized, so a /dev/shm without room for the ring is NoSpace at creation, with the half-made object unlinked, rather than SIGBUS at the first touch of a page; a reservation a signal interrupts is tried again, eight attempts in all (RESERVE_ATTEMPTS), and then reported the same way with EINTR. The envelope store, the single-slot region and the NUMA backing reserve their pages the same way, and the region’s create creates or attaches as the ring’s does.
4.6 What would refute this chapter
A reader that returns a Pulse whose bytes are not all from one publish, a writer that leaves a slot permanently unreadable after a crash, a reader that hangs on a dead writer, a region opened without a size check that faults on first access, a region whose header records another capacity or slot size than the caller’s and maps anyway, a lapped reader that receives a publish twice or a newer lap under an older index, a create that resizes or clears a live ring, or a creation that a full /dev/shm answers with SIGBUS rather than an error, each contradicts this chapter. The reference’s torn-read, crashed-writer, region-size, header, overrun, read-position, create and no-space tests are the checks.
5. The envelope store
Reference · normative for the reference implementation · the content-addressed shared-memory store beside the ring; other implementations MAY store envelopes differently and conform through chapter 8
When a Pulse’s envelope_ref is not zero, the envelope it names lives in an envelope store: a shared-memory circular buffer of envelopes in their canonical encoding that sits beside a ring. Readers resolve the reference by content identifier, so two Pulses that reference the same content share one entry. The reference type is core_store::EnvelopeStore; the layout below is version 2, adopted in decisions 0008 and 0011 of the decision record.
5.1 Region layout, version 2
| Bytes | Contents |
|---|---|
| 0 to 8 | The write cursor, atomic u64: a claim counter that hands out cursors and proves nothing about contents. |
| 8 to 12 | The magic PMES, stored last when the region is created, with release ordering. |
| 12 to 16 | The layout version, 2. |
| 16 to 20 | The capacity in slots. |
| 20 to 24 | The maximum entry size in bytes, 65,536 in this build. |
| 24 to 32 | Busy claims, atomic u64: claims that found their slot held or taken and gave up their cursor. |
| 32 to 64 | Reserved, zero. |
| 64 onward | capacity slots of 64 + 65,536 bytes each. |
Each slot begins with a 64-byte header: a sequence word at 0 to 8 (0 never written, 2·lap + 1 while a write is in progress, 2·lap + 2 once published, where lap is the cursor divided by the capacity), the envelope’s identifier at 8 to 24, the body length at 24 to 28, and reserved zeros to 64; the body, the envelope’s canonical encoding (3.1), follows and is at most the maximum entry size, so an envelope with no type name and a raw payload carries up to 65,486 payload bytes. A store for the runtime’s 256 slots is a little over sixteen megabytes.
An implementation that opens a store MUST check the region’s size and then its header, and refuse a region whose magic, version, capacity or maximum entry size disagree with its own (LayoutMismatch). A version-1 region, whose bodies were JSON, is refused on its version. There is no migration, because no store outlives the daemon that created it. A store is created as a ring is: the region is sized and, on Linux, its pages are reserved with posix_fallocate before the header is written, so shared memory without room for it is NoSpace at creation rather than SIGBUS later; a store the daemon’s hub cannot create for lack of room is logged, and the hub runs without persistence. A capacity of zero, above the header’s 32-bit field, or whose region would not fit a file offset is refused (InvalidCapacity) by open and by every creator alike, before anything is opened or created.
5.2 Writer protocol
To store an envelope, a writer:
- encodes it canonically and refuses a body larger than the maximum entry size (
EnvelopeTooLarge, naming the canonical size) before claiming anything; - claims a cursor by incrementing the write cursor;
- claims the slot for that cursor by moving its sequence word from an even value no newer than
2·lapto2·lap + 1with a compare-and-swap; a claim never starts from an odd word, so no writer ever writes a slot another writer holds; - if the slot is held (another writer mid-store, or one that died there) or already taken by a newer lap, leaves it alone, adds one to the busy-claims counter, and returns to step 2 for a fresh cursor, up to four cursors in all, after which the store fails with
SlotBusynaming the last cursor tried; - issues a release fence and writes the identifier, the length and the body;
- stores
2·lap + 2.
The cursor is handed out before the body exists, so the cursor alone is never evidence that a slot holds data; the sequence word is. A cursor given up in step 4 is never written, and readers skip it.
5.3 Reader protocol
A reader trusts a slot only when its sequence word is the published value for the cursor it wants both before and after the body is copied out, with an acquire fence between the copy and the second load, and it copies the body before decoding it rather than decoding shared memory in place. As defence in depth against a copy corrupted or torn outside that protocol, it returns an envelope only if its copy decodes as exactly one canonical envelope whose identifier is the slot’s and whose content verifies against that identifier; nothing is returned unverified. A reader spends its retry budget only on the writer of the cursor it wants, and skips at once a slot held by an older lap’s writer, because that cursor’s own writer routed around the held slot rather than wait for it.
fetch by identifier scans the slots and skips any that is unwritten, in progress, held by an older lap, torn, has an implausible length or does not decode and verify, rather than ending the scan. read_since(cursor) returns the envelopes for consecutive cursors from the later of the requested cursor and the oldest cursor still in the region; it skips a cursor whose own writer has not published within the reader’s budget (stalled or dead mid-store, or yet to claim), skips at once a cursor whose slot is held by an older lap, skips a slot lapped during the call, and returns the cursor to resume from, so a streaming reader neither wedges on a dead writer nor mistakes a lapped slot’s stale body for new data. Nothing older than write cursor − capacity is ever returned.
5.4 What this store is and is not
The store is a cache as much as a store: identical content is stored once and a thousand Pulses carrying the same event reference one entry. Its maximum entry size is a deployment tunable recorded in the header, not a limit on the envelope contract (3.7). Sizing the region to the envelopes actually stored, rather than reserving the maximum per slot, is a hub-configuration question outside this chapter. A writer that dies between its odd and even marks holds its slot until the region is recreated; it loses its own envelope and no other, because each later claim on that slot routes to the next cursor, at the cost of one cursor and one busy claim per lap. Taking over a stale mark was considered and rejected: the age of a mark cannot tell a dead writer from a preempted one, and taking over a live writer’s slot would tear its store.
5.5 What would refute this chapter
A reader that returns a body from a slot whose sequence word was not the published value for the cursor it asked for, or a body that does not verify against the slot’s identifier; a read_since that returns an envelope older than the window or skips silently past a live slot; a second writer that overwrites a slot another writer holds; a claim that gives up before its fourth cursor or leaves the busy count unchanged; or an open that maps a region of another layout, each contradicts this chapter. The reference’s layout, lapping, held-slot and publish-visibility tests are the checks; the last runs four readers against a writer for four hundred milliseconds and asserts that every returned envelope verifies and carries the cursor arithmetic.
6. The UDP transport
Reference · normative for the reference transport · how `pulse-wire-udp` frames a Pulse and its envelope in one datagram, and the feedback, parity and handshake around it; another transport MAY frame differently and conforms through its own vectors
The reference transport carries one Pulse and at most one envelope per datagram, checks every datagram with a CRC, and layers three optional mechanisms on top: acknowledgement-driven backpressure, negative acknowledgement with retransmission from a send cache, and single-parity forward error correction. A capability handshake lets two peers agree which of those to use. Amber in every figure.
6.1 Datagram framing
One datagram is one frame, little-endian, with no padding:
| Field | Size | Contents |
|---|---|---|
magic | 4 | PMSW as little-endian bytes, 0x57534D50; tells Pulse Mesh traffic apart from anything else on the socket. |
version | 1 | 1. Bumped on any layout-incompatible change. |
seq | 8 | A per-run monotonic sequence number; feedback refers to it. |
send_ns | 8 | The sender’s clock in nanoseconds, so a receiver can compute one-way latency without a side channel. |
pulse | 128 | The Pulse’s bytes, verbatim. |
n | 4 | The envelope length, 0 to MAX_ENVELOPE. |
envelope | n | The envelope’s canonical bytes, when present. |
crc32c | 4 | A Castagnoli CRC over every preceding byte. |
The header is 153 bytes and the trailer 4. The transport assumes a 1,500-byte path MTU less 20 bytes of IPv4 and 8 of UDP, so MTU_BYTES = 1472 and MAX_ENVELOPE = 1315; a sender MUST reject a larger envelope at encode time, and the transport does not fragment. A receiver drops a datagram whose CRC does not match, silently, because this tier is fire and forget; it rejects a datagram whose magic or version differ; and when an envelope is present it decodes it canonically and verifies its identifier, rejecting a mismatch (EnvelopeIdMismatch). The reference publishes these constants and the frame vectors in its wire_format tests.
6.2 Feedback: acknowledgement and retransmission
Two 12-byte feedback datagrams travel from the receiver to the sender’s feedback socket:
| Frame | Bytes | Contents |
|---|---|---|
| ACK | PMSA (0x41534D50) then a u64 | The receiver’s cursor: one past the highest sequence it has seen, whether or not every lower sequence has arrived. |
| NACK | PMSN (0x4E534D50) then a u64 | One sequence number the receiver is missing. |
Backpressure. The receiver sends an ACK every acknowledgement period, one millisecond by default. The sender keeps the highest cursor it has seen (an ACK can only move it forward, so a replayed or reordered ACK is harmless) and stalls a send while the frames in flight beyond that cursor would exceed target_queue_depth, sixteen by default, backing off through spins and yields into fifty-microsecond sleeps until the cursor moves or the stall deadline passes (stall_timeout, five seconds by default, set with with_stall_timeout, None for no deadline), when the send fails with Stalled and the frame is not sent. A cursor ahead of the sender’s next sequence, which a receiver that outlived a restart of the sender leaves behind, counts as nothing in flight. A target queue depth of zero is refused when backpressure is enabled (InvalidQueueDepth), because it would admit no frame.
Retransmission. Only an arrival above the highest sequence seen reveals missing sequences, the ones between the two, and the receiver stages exactly those, once each, in a set capped at its recovery window, which is the sender’s send-cache capacity (1,024 frames by default). A gap wider than the window keeps only its newest window of sequences staged; the rest are given up without a request and counted (nack_drop_count), as are the sequences before the first frame of a receiver that joins a stream late, and the receiver moves its head past what it gave up and counts a resync. It emits at most sixteen NACKs per acknowledgement period, requests a sequence again after four periods if it is still missing, and gives up on it after three requests. The sender keeps the last 1,024 encoded frames in a cache, accepts NACKs into a queue bounded by that capacity (a NACK arriving at a full queue is dropped and counted), drains at most sixteen NACKs per frame sent, for a batch as for a single send, and retransmits the cached bytes exactly as first sent, to its data destination and never to the NACK’s source, so a forged NACK cannot redirect traffic.
Admission. The sender’s feedback socket MAY be restricted to one peer address (feedback_peer); a datagram from any other address is dropped and counted. Without the restriction it accepts feedback from any source, which is the historical behaviour. Every mechanism exposes its counters: retransmits, cache misses, rejected feedback and dropped NACKs at the sender; NACKs sent, sequences given up on and gap resyncs at the receiver.
Batches. A batch send (send_batch) follows the same rules frame by frame. On Linux the batch goes out in sendmmsg(2) calls and elsewhere through the single send, and either way backpressure holds every frame to the window the receiver’s cursor leaves open, a frame that waits past the stall deadline ends the batch at its index (FrameError) with the frames before it sent and the sequence counter advanced only for them, so the caller can resend the unsent tail under the sequence numbers it left unused; the NACKs pending for a call’s frames, sixteen at most per frame, are retransmitted before the call; every frame sent is cached for retransmission; and with parity enabled no call runs past the last frame of a parity window, so each parity datagram follows the frame that completes its window before any later frame. A batch larger than MAX_BATCH_FRAMES is refused before anything is sent (BatchTooLarge). Frames go out in order, and on an error the frames before the failure were sent and the rest were not: on Linux, Io means the kernel refused the batch’s first frame and nothing went out, and PartialSend means it took only some of the frames it was given, without saying why. A batch receive (recv_batch) takes every datagram through what the single receive does, on every platform: a data frame advances the ACK cursor and feeds gap tracking and NACK staging when backpressure is on, so a sender under backpressure is not stalled by a receiver that reads in batches, and the FEC decoder when FEC is on; a parity datagram goes to the decoder, or is dropped when FEC is off, and is never reported as an error; frames the decoder rebuilds are delivered after the frames decoded from the call’s own datagrams, in the slots left, and the rest are held for the next receive call, which returns held frames first and, when it finds any, does not read the socket. A call with nothing held blocks until the first datagram arrives or the read timeout passes, then takes the datagrams already queued without waiting (on Windows each further read waits as the first does); a call whose datagrams were all parity delivers no frames; and a failure to read the first datagram, the timeout included, is the batch’s Io error, after which nothing was read.
Delivery. The transport promises nothing about how many times a sequence is delivered. A sequence can arrive zero times, once, or more than once: a late original after its retransmission, or a frame rebuilt from parity followed by its late original. The receiver does not suppress duplicates; a caller that needs each sequence once filters on RecvMeta::seq, the frame’s sequence number, which every delivery carries.
6.3 Forward error correction
With FEC enabled, the sender groups K consecutive frames into a window (K at least 1; sixteen by default; a window size of zero MUST be rejected) and after the K-th frame emits one parity datagram on the same socket:
| Field | Size | Contents |
|---|---|---|
magic | 4 | PMSF (0x46534D50). |
window_start | 8 | The sequence number of the window’s first frame. |
window_n | 2 | K, so a receiver detects a mismatched configuration. |
parity_len | 4 | The length of the parity, the longest frame in the window. |
parity | parity_len | The byte-wise exclusive or of the K encoded frames, each zero-padded to the longest. |
A receiver that holds K − 1 of the window’s frames and the parity rebuilds the missing frame’s bytes by the same exclusive or and passes them through the ordinary decode path, CRC included; a window with two or more losses falls through to retransmission. Parity trades sender and receiver CPU for the round trip a NACK would cost on a long path.
6.4 The capability handshake
Two peers start on plain UDP and exchange a 48-byte Hello, defined in mesh-capability so every transport backend speaks the same one:
| Field | Size | Contents |
|---|---|---|
magic | 4 | PMSH (0x48534D50). |
version | 2 | The protocol version the sender speaks, at most 3 in this revision. |
capabilities | 4 | A bit field: BACKPRESSURE 1, NACK 2, FEC 4, AF_XDP 8, DPDK 16, TLS_443 32. |
registry_hash | 32 | The BLAKE3 hash of the sender’s context registry. |
reserved | 6 | MUST be zero; receivers ignore it. |
The outcome is the intersection of the two capability fields; a peer upgrades only to what both sides announced. A registry-hash mismatch is advisory: the peers confirm that their books of scopes agree before promoting traffic. The socket handoff that follows an agreed upgrade belongs to each backend, not to this chapter.
6.5 What would refute this chapter
The frame vectors in the transport’s wire_format tests give the bytes of known frames; a loopback test carries Pulses and envelopes end to end; the backpressure, retransmission, gap-bound, queue-bound, feedback-filter and FEC tests exercise each mechanism at its limits. A sender that fragments, a receiver that accepts a datagram with a bad CRC or an unverified envelope, a retransmission that goes to a NACK’s source, an unbounded NACK queue, or a parity window of zero frames each contradicts this chapter.
8. Conformance and the normative surface
Reference · normative · what a conformant implementation reproduces, and which changes to this standard require a draft revision
8.1 What conformance is
Conformance is byte equality against the vectors under pulse/pulse-spec/test-vectors, not similarity of API. A conformant implementation:
- MUST produce byte-identical canonical bytes and identifiers to the vectors for the inputs they define (
pulse-layout-known-good,envelope-raw-known-good,envelope-nested-3-level, each as JSON fields and as hex bytes); - MUST accept byte-identical input that any other conformant implementation produces for the same fields;
- MUST treat the canonical envelope size as unbounded (3.7);
- MUST accept nested envelopes to a depth of 32 and MAY reject deeper ones (3.4);
- MUST validate UTF-8 in a type name and MUST reject unknown type-name and payload-variant tags (3.1, 3.3);
- SHOULD expose self-verification (3.5);
- SHOULD treat an unknown instruction as
Null(2.2).
The vectors are language-neutral and are the source of truth for byte-level interoperability. They are regenerated only by the tool under pulse/pulse-spec/tools, and a change to them is a draft revision (8.3).
8.2 How conformance is shown
In this repository the codec’s own tests assert the vectors; the WebAssembly binding exposes check_fixture, which runs them in the browser; and the Python, Node and C bindings carry byte-equality tests against the same files. The port protocol has fixtures of its own, under mesh/mesh-port-protocol/fixtures, which the Rust types write and the two thin clients’ tests read (chapter 7). A satellite implementation shows conformance the same way: it reproduces the vectors, and its entry in modules.toml, which defines the registration fields and lists no satellite today, names the surface it conforms to.
The Lexicon™ anchors the published catalog of envelope types: mesh-lex computes a Merkle root over its public entries, the daemon logs that root at start, and pulse/pulse-spec/lex-root.txt records the root the release was cut with, so an implementation can tell which catalog a peer speaks.
8.3 The normative surface
| Surface | Status | On change |
|---|---|---|
| Chapter 2: the Pulse layout, alignment and instructions | Normative | Draft revision |
| Chapter 3: the canonical envelope bytes, the identifier, the variants, the nesting rules | Normative | Draft revision |
| Chapter 4: the ring protocol | Normative for shared-memory substrates | Draft revision |
| Chapter 5: the store layout | Reference implementation | Layout version bump, recorded in the decision log |
| Chapter 6: the UDP framing, feedback, parity and handshake | Normative for the reference transport | Draft revision when the framing changes; other transports unaffected |
| Chapter 7: the port protocol | Normative for the daemon’s clients | Protocol version bump |
The vectors under pulse/pulse-spec/test-vectors | Normative, the executable contract | Draft revision on any addition or change |
The registration fields modules.toml (profiles repository) defines for a satellite (tier, registry status, targets, conformance surface) | Normative | Draft revision when a meaning changes; entries added without a change of meaning do not |
| The reference crates, the tools, the examples and this book | Reference and informative | Release versions; not a revision of the standard |
A change to the reference implementation that leaves the vectors’ output unchanged is not an event for the standard. A change that alters the vectors’ output is a draft revision, and the superseded rule stays in the text, marked, with the revision that replaced it.
Appendix A. BLAKE3
Reference · informative · the hash the content identifier is built on
The content identifier of chapter 3 is the first 16 bytes of a BLAKE3 hash over the preimage of section 3.2. BLAKE3 is specified by its authors in the BLAKE3 paper and reference implementation, published at github.com/BLAKE3-team. The standard uses the plain hash mode with the preimage’s own domain tag as its first bytes; it does not use BLAKE3’s keyed or key-derivation modes.
The reference implementation hashes with the Rust blake3 crate. The WebAssembly binding compiles the same crate. The Python and Node bindings and the satellite implementations use independent BLAKE3 implementations, and the vectors of chapter 8 are what show that all of them produce the same 16 bytes for the same preimage; that equality is the cross-language byte-equality contract of section 3.6.
Truncating a 32-byte BLAKE3 output to 16 bytes keeps the identifier the size of a node id and a Pulse’s envelope_ref; the 128-bit identifier is a content address, not a signature, and the standard makes no claim about it beyond collision resistance at that width for the purpose of addressing.
Workspace Layout
Reference · the two tiers, the leaves beside them, and the repositories built on this one, for anyone finding their way around the tree
This repository holds the bottom two tiers of the Pulse Mesh stack. A tier depends only on the tiers below it, and nothing here depends on anything outside the repository: a consumer that wants the wire codec alone depends on pulse-canon and pulls in neither the ring nor the transport.
| Tier | Path | What lives here |
|---|---|---|
| 0 — cornerstone | pulse/ | pulse-canon (the Pulse atom + canonical envelope codec), pulse-spec/ (the conformance test vectors, the lex root, and the generators that (re)produce them), and the vocabulary crates: mesh-lex (the Lexicon), mesh-context and mesh-capability (the context and capability registries) |
| 1 — engine | core/ | reference SeqLock ring, envelope store, region map, backoff strategies, the transport trait, the reference UDP transport, the CRC32C integrity primitive, and mesh-stage-registry, the register_stages! macro every ring hub composes with |
Beside the tiers sit the leaves: they consume the tiers and are consumed by nothing.
| Path | What lives here |
|---|---|
tools/ | pulse-cli, the pulse command line (encode, decode, verify, send, receive, the loopback bench; see Across a network), and gen-lex-root, which regenerates pulse/pulse-spec/lex-root.txt |
examples/ | the three programs the book includes: hello-pulse, the UDP loopback (Across a network) and the embedded ring and store (The ring inside your process) |
bindings/ | the C, Python, Node and WebAssembly bindings over the codec and the UDP transport; not workspace members, each builds from its own directory (Language bindings) |
fuzz/ | the cargo-fuzz harness over the standard’s parsers of untrusted bytes; not a workspace member: cargo +nightly fuzz run <target> --fuzz-dir fuzz runs one, and just check-fuzz builds them all |
The repositories built on this one
Tiers 2 and 3 are two repositories that depend on this one, as a sibling checkout (../pulse-mesh) while developing and as a pinned tag at a release. The daemon is tier 2 and the dev ring: mesh-hub and its port protocol, the hub runtime, the Rust client, and the pms tools. The profiles are tier 3: the enterprise, HFT and robotics rings, each the same four crates (core, functions, stages, hub) composed through the register_stages! macro this repository holds. Neither adds anything below this repository; where a change here is needed for one of them, it is made here first.
Deciding where new code goes
CONTRIBUTING.md frames this as a four-question test: does the code touch the canonical Pulse atom, touch the canonical envelope, cross a ring boundary, or sit on the hot path? Code that touches the atom or the envelope belongs in pulse/; a hot-path primitive every consumer may need belongs in core/; a stage that crosses a ring boundary belongs in a ring, in the profiles; anything that only uses the tiers (a tool, an example, a binding) is a leaf here. Record any non-obvious classification call as a new ADR under decisions/ rather than only justifying it in the PR.
Cornerstone: pulse/
Reference · the cornerstone tier, crate by crate
This is the bottom of the four-tier stack: the canonical Pulse atom and envelope codec, plus the conformance artifacts that pin their wire format. See also the crate-level notes in pulse/README.md.
pulse-canon
The canonical primitive crate, and it’s intentionally minimal. It defines exactly three things:
Pulse— the 128-byte, cache-aligned wire-format atom.PulseEnvelope— the variable-size canonical payload carrier.NodeId— the 16-byte node identifier.
Everything with more behavior — engine primitives, fabric primitives, domain surfaces — lives one or more tiers up: the engine in core/, and the fabric and the domain surfaces in the daemon and profiles repositories. If you find yourself wanting to add a helper function to pulse-canon, that’s usually a signal it belongs in core/ instead.
pulse-spec
Not a Rust crate — a directory of conformance-spec artifacts, the tool that regenerates the vectors, and the root the tools under peripheral/tools regenerate:
lex-root.txt— the published Merkle root over the canonicalmesh-lexregistry.test-vectors/— language-neutral conformance fixtures: a known-good 128-byte Pulse layout, a canonical Raw envelope, and a 3-level-deep Nested envelope. Any language implementation (Rust, the C/Python/Node bindings, a future port) can replay these to verify byte-for-byte compatibility with the reference codec.gen-lex-root(tools/gen-lex-root; a tool over themesh-lexregistry, so it lives with the tools rather than in tier 0) — regenerateslex-root.txtinto the path given,cargo run -p gen-lex-root -- pulse/pulse-spec/lex-root.txt, by computing the BLAKE3 Merkle root over everyBoundary::Publicentry in themesh-lexregistry. Today that registry is empty, so the root reduces to the domain-separated empty sentinelBLAKE3("mesh-lex/empty/v1").tools/gen-test-vectors— regeneratestest-vectors/.
Regenerate both tools’ output before treating a wire-format change as final. The wire format is the thing every downstream language binding depends on longest — a change here has the widest and longest-lived blast radius of anything in the workspace.
Engine: core/
Reference · the engine tier, crate by crate
Reference data structures and transports built directly on the pulse-canon atom: shared-memory IPC primitives, backoff strategies, the transport trait family, the reference UDP transport, and integrity checking. See also core/README.md.
| Crate | Purpose |
|---|---|
core-backoff | Backoff — a three-phase spin-wait state machine for OS-visible stalls (the UDP sender uses it while waiting for the receiver’s cursor): a bare-metal hot spin (steps 0–1000, ~ns latency), then an OS yield (1001–5000, ~µs latency, near-zero CPU), then a sleep (5001+, 1µs by default, ~0% CPU). reset() snaps back to phase 1 the instant progress is observed. Bounded SeqLock retries stay bare spins and hardware monitor waits use hft-functions’ WaitStrategy; the crate doc says which to pick. |
core-region | PulseRegion — a single-slot POSIX SHM mmap region holding one Pulse, protected by a SeqLock embedded in the pulse’s seq_lock field: writers increment to odd then even, readers retry on odd or on pre/post disagreement. create creates the region or attaches to the one under the name, as the ring’s does, never resizing or clearing it, and on Linux its page is reserved when the object is sized. The crate also holds shm, the one shared-memory mechanism: name validation, exclusive creation abandoned on a later failure, sizing with the Linux page reservation, the open-then-map step at which a header is read through the descriptor before any page is touched, the mapping that unmaps itself when dropped, and the unlink that removes a name only while it still names the caller’s object. The ring, the store and the NUMA backings map through it and keep their own headers, geometry and errors (ADR-0013). |
core-ring | PulseRing — the multi-slot SeqLock-protected SHM ring buffer, with a documented byte layout (write cursor, total writes, read cursor, then the ring’s own description: magic, header version, capacity and slot size, then N × 128-byte slots); open refuses a header that disagrees with the caller, and open_existing takes the geometry from the header. A publish takes the handle’s &mut, so a handle has one writer, and always succeeds, overwriting the oldest slot; a reader that lands on a slot mid-write retries within a bounded budget, and an odd counter left by a crashed writer is re-based by the next publish into that slot. read_position is the read for a poller that walks the write positions in order: each Pulse once, or the position counted lost with the next one to read, never a later lap under an earlier position. create creates a ring or attaches to the one under the name, never resizing or clearing a live ring, and is_creator says which happened, which the hub runtime uses to make the creating hub the ring’s one writer; on Linux a new object’s pages are reserved when it is sized, so a /dev/shm without room is NoSpace at creation rather than SIGBUS later. |
core-store | EnvelopeStore — a SHM circular buffer of serialized PulseEnvelopes, referenced from a Pulse via envelope_ref. Each slot is a SeqLock word, a 16-byte ID and a length prefix ahead of up to 64 KiB of the envelope’s canonical bytes (layout version 2, ADR-0011); a writer publishes a slot only after its body is written and routes around a held slot to a fresh cursor, the header carries a magic, a version and a busy-claim counter that open checks, and readers verify every body they return, fetch by ID, or stream new entries via read_since. |
core-transport | The public PulseTransport trait family (PulseSender/PulseReceiver) that concrete transport backends implement. Deliberately one-way (not bidirectional) to match per-thread halves, excludes measurement metadata from the hot path, avoids allocation in recv_into (writes into a caller-owned buffer), and returns concrete — not boxed — errors via TransportError. |
pms-crc32c | CRC32C integrity-trailer utilities (compute_pulse_crc/verify_pulse_crc) over the Pulse data area (bytes 8..124 of the 128-byte slot) — Module M10 of the Pulse Mesh Standard. |
pulse-wire-udp | The reference UDP transport: one datagram is one Pulse plus an envelope of at most 1,315 bytes, framed with magic, version, sequence, timestamp and a CRC32C trailer, never fragmented. Three mechanisms are off by default and switched on per side: backpressure (an acknowledged cursor and a bounded in-flight window), NACK retransmission from a send cache, and per-window parity. |
mesh-stage-registry | Tier 1 since the split (it depends on nothing and every ring hub uses it). #![no_std]. Provides the register_stages! macro that every ring’s *-hub crate calls once to enumerate its functions and stages. It emits RING_MANIFEST, a constant listing the hub’s declared function and stage modules, and imports every listed path, so a hub that names a module that does not exist does not compile; each hub’s tests assert that manifest against the layers the hub re-exports. |
Where to plug in
Downstream code should generally depend on core-transport (the trait) and a concrete backend — pulse-wire-udp today, with AF_XDP/DPDK/TLS-443 backends as separate crates elsewhere in the workspace (the profiles repository’s plugins/hft-xdp, enterprise_ring’s TLS stage). Reach into core-ring/core-store/core-region directly only if you’re implementing a new transport or ring primitive yourself, not as a general-purpose API.
Crate index
Reference · generated · every workspace crate by tier, with the description its manifest carries
A tier depends only on the tiers below it; the peripheral surface depends on all of them and is depended on by none. The description column is each crate’s Cargo.toml description, so this table is exactly what cargo metadata reports. The status column is the crate’s package.metadata.pulse-mesh.status: pre-integration for a crate the tree ships but does not use yet (ADR-0014), partial for one in use some of whose public entry points return a deferred error (ADR-0017), blank for one that is finished and wired in.
The canonical primitive (tier 0, pulse/)
| Crate | Path | Status | Description |
|---|---|---|---|
gen-test-vectors | pulse/pulse-spec/tools/gen-test-vectors | Regenerate the language-neutral conformance fixtures under test-vectors/. | |
mesh-capability | pulse/mesh-capability | Pulse Mesh capability negotiation: per-node feature flags + version negotiation handshake. | |
mesh-context | pulse/mesh-context | pre-integration | Pulse Mesh Context Registry / Book of Standards: 16-bit context_id + 16-bit subscope namespace partitioning of the Pulse scope field. |
mesh-lex | pulse/mesh-lex | Pulse Mesh public conformance substrate — minimum-viable lex registry + Merkle root over the registered catalog. | |
pulse-canon | pulse/pulse-canon | Pulse Mesh canonical primitive: the 128-byte Pulse + PulseEnvelope codec + NodeId. Tier 0 (the shape). |
The engine (tier 1, core/)
| Crate | Path | Status | Description |
|---|---|---|---|
core-backoff | core/core-backoff | Pulse Mesh backoff for OS-visible stalls: spin, then yield, then sleep in fixed steps. Tier 1 (the engine). | |
core-region | core/core-region | Pulse Mesh shared-memory mechanism (shm_open, size and reserve, mmap, unlink), NUMA placement (hugepage backings, mbind, thread pinning) and PulseRegion, the single-slot region. Tier 1 (the engine). | |
core-ring | core/core-ring | Pulse Mesh SHM-backed SeqLock ring buffer (single-writer, N-reader). Tier 1 (the engine). | |
core-store | core/core-store | Pulse Mesh EnvelopeStore: SHM circular buffer for variable-size envelopes with content-addressed lookup. Tier 1 (the engine). | |
core-transport | core/core-transport | Pulse Mesh PulseSender / PulseReceiver trait family. Substrate-agnostic transport contract. Tier 1 (the engine). | |
mesh-stage-registry | core/mesh-stage-registry | Compile-time stage / function registry macro for ring hubs. The register_stages! macro enumerates a hub’s functions and stages and emits RING_MANIFEST, so the inner-ring four-crate composition is asserted in source and testable. | |
pms-crc32c | core/pms-crc32c | Pulse Mesh CRC32C trailer: compute and verify the checksum a Pulse carries in its last four bytes. Module M10. Tier 1 (the engine). | |
pulse-wire-udp | core/pulse-wire-udp | UDP framing for the Pulse Mesh Standard wire transport: one Pulse + variable-size envelope per datagram, MTU-aware, CRC32C-protected, with optional backpressure, NACK retransmission and FEC. |
The peripheral surface (tier peripheral, tools/, examples/)
| Crate | Path | Status | Description |
|---|---|---|---|
example-01-hello-pulse-rust | examples/01-hello-pulse-rust | Example 01 — encode + decode a PulseEnvelope, print its BLAKE3 content hash. | |
example-02-loopback-udp-rust | examples/02-loopback-udp-rust | Example 02 — a sender and a receiver thread exchanging Pulses over UDP loopback in one process. | |
example-03-embed-ring-rust | examples/03-embed-ring-rust | Example 03 — the shared-memory ring and the envelope store used directly, without the daemon. | |
gen-lex-root | tools/gen-lex-root | Regenerate the published lex_root (pulse/pulse-spec/lex-root.txt) for the current public mesh-lex registry state, into the path given. | |
pulse-cli | tools/pulse-cli | The standard’s own command line, pulse: encode, decode and verify envelopes, send and receive Pulses over UDP, and the loopback smoke bench, over pulse-canon and pulse-wire-udp alone. Peripheral. |
Error catalogue
Reference · generated · every public error type in the workspace, variant by variant, with the meaning its documentation states
The table is read from the source, so a variant that exists is listed and a variant that is gone disappears. The troubleshooting guide gives remedies for the errors a user meets from the command line.
ShmError in core/core-region
Defined in core/core-region/src/lib.rs.
| Variant | Meaning |
|---|---|
Open | shm_open(3), fstat(2) or a header read failed; the field is the errno. |
Truncate | ftruncate failed; the field is the errno. |
NoSpace | Linux could not reserve a new region’s page: posix_fallocate(3) failed, with ENOSPC on a full /dev/shm, ENOMEM, or EINTR after [RESERVE_ATTEMPTS] tries. The object was unlinked and nothing was mapped. Never returned off Linux. |
Map | mmap failed; the field is the errno. |
InvalidName | The name contains a NUL byte. |
RegionTooSmall | The object is smaller than a region, [REGION_SIZE] bytes. |
LayoutMismatch | The header is not a region’s: the magic is not PMRG (not a region, or not yet published), or the layout version is not [LAYOUT_VERSION]. Nothing was mapped. |
NumaError in core/core-region
Defined in core/core-region/src/numa.rs.
| Variant | Meaning |
|---|---|
HugePageAlloc | Making a memfd ([allocate_hugepage_memfd]) or duplicating a descriptor ([dup_fd]) failed; the string says what and why. |
NumaPin | Pinning a thread or binding memory to a NUMA node failed: sched_setaffinity(2) or mbind(2) refused, the node is past the 64 a binding can name, or a CPU number is past the CPU_SETSIZE (1024) a cpu_set_t can name. |
UnsupportedPageSize | Caller requested a page size the kernel doesn’t expose. Check /sys/kernel/mm/hugepages to see what’s available. |
LibnumaMissing | The operation is Linux-only: binding memory, detecting a node, pinning a thread and hugepage memfds return this elsewhere. Nothing links libnuma; on Linux these are the kernel’s system calls. |
Io | Underlying I/O failure (typically while creating or sizing the SHM file). |
Error in core/core-region
Defined in core/core-region/src/shm.rs.
| Variant | Meaning |
|---|---|
Open | shm_open(3) or the fstat(2) after it failed; the field is its errno (EEXIST from an exclusive creation whose name already holds an object, ENOENT from an open of a name that holds none, EACCES from a runtime that refuses POSIX shared memory, EBADF from a descriptor that is not open). |
Truncate | ftruncate(2) could not size the new object; the field is its errno (EFBIG, ENOSPC). |
NoSpace | Linux could not reserve a new object’s pages: posix_fallocate(3) failed, with ENOSPC on a full /dev/shm, ENOMEM, or EINTR after [RESERVE_ATTEMPTS] tries. Never returned off Linux. |
Map | mmap(2) failed; the field is its errno. |
InvalidName | The name holds a NUL byte, which no C string can carry. |
TooSmall | The object is shorter than the caller needs: have bytes where need are needed. An object at size zero is one a creator has made and not yet sized. |
RingError in core/core-ring
Defined in core/core-ring/src/lib.rs.
| Variant | Meaning |
|---|---|
Open | shm_open(3), fstat(2) or a header read failed; the field is the errno. |
Truncate | ftruncate failed; the field is the errno. |
NoSpace | Linux could not reserve a new ring’s pages: posix_fallocate(3) failed, with ENOSPC on a full /dev/shm, ENOMEM, or EINTR after [RESERVE_ATTEMPTS] tries. Nothing was mapped: a creator by name unlinked its object, and [PulseRing::from_fd] left the descriptor open and unformatted. Never returned off Linux. |
Map | mmap failed; the field is the errno. |
Unlink | shm_unlink failed; the field is the errno. |
InvalidName | The ring name contains a NUL byte, or is not a valid hub name. |
InvalidCapacity | Zero slots; or, from a constructor that sizes or formats a region, a ring whose size, HEADER_SIZE + capacity × slot size, does not fit a usize and a file offset. Nothing was created, opened or mapped. |
BatchTooLarge | A batch publish was given more pulses than the ring has slots; nothing was written. Publish at most capacity pulses per batch. |
Stalled | A blocking publish waited its whole timeout and the slowest reader never freed the room it needed: it stopped, died, or there is none. Nothing was published, so the call can be retried. |
NoReaderTable | [PulseRing::register_reader] on a ring made without a reader table. |
ReadersFull | Every line of the ring’s reader table is taken. |
InvalidReaders | A reader table of max_readers lines: it needs from 1 to [MAX_READERS]. Nothing was created. |
NameTooLong | A derived shared-memory name exceeds the platform limit. |
RegionTooSmall | The object is smaller than the ring the caller asked to map, which would fault past its end. |
LayoutMismatch | The header does not describe the ring the caller maps: the magic is not PMSR (not a ring, or not yet formatted), or the layout version, capacity or slot size differs. |
CrcMismatch | [PulseRing::read_slot_verify_crc] found a slot whose CRC32C trailer does not match its data. |
EnvelopeStoreError in core/core-store
Defined in core/core-store/src/lib.rs.
| Variant | Meaning |
|---|---|
Open | shm_open(3), fstat(2) or a header read failed; the field is the errno. |
Truncate | ftruncate failed; the field is the errno. |
NoSpace | Linux could not reserve a new store’s pages: posix_fallocate(3) failed, with ENOSPC on a full /dev/shm, ENOMEM, or EINTR after [RESERVE_ATTEMPTS] tries. The object was unlinked and nothing was mapped. Never returned off Linux. |
Map | mmap failed; the field is the errno. |
Unlink | shm_unlink failed; the field is the errno. |
InvalidName | The name contains a NUL byte. |
InvalidCapacity | Zero slots, more than the header’s 32-bit capacity field can record, or so many that 64 + capacity × slot size does not fit a usize and a file offset. Nothing was opened, created or mapped. |
RegionTooSmall | The object is smaller than the store the caller asked to map, or, for [EnvelopeStore::open_existing], than a header or the store its header describes. |
LayoutMismatch | The header does not describe the layout this build uses. |
SlotBusy | Each of the [CLAIM_ATTEMPTS] cursors tried found its slot held or taken by a newer lap, or named a lap too large to mark (a write cursor corrupted in shared memory). Nothing was written; readers skip every cursor given up. |
InvalidIndex | An index of buckets buckets for a store of capacity slots: the count must be a power of two no larger than the capacity rounded up to one. Nothing was created. |
EnvelopeTooLarge | The envelope’s canonical encoding exceeds [MAX_ENTRY_SIZE] bytes. |
TransportError in core/core-transport
Defined in core/core-transport/src/lib.rs.
| Variant | Meaning |
|---|---|
Io | Underlying I/O failure (socket, file, kernel). |
BufferTooSmall | The caller’s envelope buffer is smaller than the incoming envelope. |
Decode | A frame failed the wire format’s validation (magic, version, length, CRC, or the backend’s own checks). |
EnvelopeTooLarge | The envelope exceeds the transport’s max_envelope(). |
Send | A send failed for a reason Io does not fit, such as the backend’s flow control stalling (the UDP transport’s backpressure window, which its opt-in congestion control sizes). |
Recv | A receive failed for a reason Io and Decode do not fit. |
FecError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/fec.rs.
| Variant | Meaning |
|---|---|
TooShort | Buffer is shorter than [FEC_HEADER_BYTES]. |
MagicMismatch | First 4 bytes don’t match [FEC_MAGIC]. |
Truncated | Length-prefix says the parity payload is parity_len bytes but the buffer doesn’t actually carry that many. |
InvalidWindow | The header declares a window of zero frames, which could rebuild nothing; refused here, before it reaches the decoder’s arithmetic. |
ReceiverError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/receiver.rs.
| Variant | Meaning |
|---|---|
Io | Socket I/O error. WouldBlock from a timeout is one of these. |
Decode | The datagram failed wire-format validation (magic, version, length or CRC). |
BufferTooSmall | The envelope does not fit the buffer given to [Receiver::recv_into]; the frame is consumed. |
InvalidPulseBytes | A decoded frame’s 128 pulse bytes are not a [pulse_canon::Pulse]: a sender bug. |
BatchRecvError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/receiver.rs.
| Variant | Meaning |
|---|---|
Io | Reading the first datagram failed, so nothing was read; a timeout (WouldBlock or TimedOut) means none arrived within the socket’s read timeout. |
BatchTooLarge | max_frames exceeded [crate::sender::MAX_BATCH_FRAMES]. |
InsufficientBuffers | envelopes.len() is smaller than max_frames. |
SealError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/sealed.rs.
| Variant | Meaning |
|---|---|
Io | The socket refused a handshake message. |
KeyFile | The key file could not be opened, created, read or written. |
KeyFileExposed | The key file can be read or written by its group or by others, or is not a regular file; it is not loaded. |
InvalidKey | Text that should name a key is not 64 hex digits. |
NotAllowed | The peer’s key is not on this node’s allowlist. |
HandshakeTimeout | No response to the handshake arrived within the timeout. |
NoSession | There is no session to the destination: [Sealing::connect] first, or the session was dropped, idle, or spent its counter. |
Crypto | The Noise library or the system’s entropy source refused. |
SenderError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/sender.rs.
| Variant | Meaning |
|---|---|
Io | Underlying socket I/O failure. |
Encode | The frame failed to encode: [WireError::EnvelopeTooLarge] for an envelope over [Sender::envelope_limit]. The sender does not fragment. |
NoDestination | No destination — caller must connect() or use [Sender::send_to]. |
Stalled | Backpressure held the send past stall_timeout: the receiver’s cursor did not come within target_queue_depth frames of the next sequence number. The frame was not sent and its sequence number not used, so the call can be retried. |
InvalidQueueDepth | [Sender::enable_backpressure] was given a target_queue_depth of zero, which admits no frame; the sender’s backpressure stays as it was. |
Seal | Sealing is installed and the frame could not be sealed: [SealError::NoSession] when no session to the destination is in use ([Sealing::connect] first). Found before the send waits on backpressure; the frame was not sent and its sequence number not used. |
BatchSendError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/sender.rs.
| Variant | Meaning |
|---|---|
FrameError | The frame at index failed: its encode, a backpressure stall past stall_timeout, or its send (on Linux, a sendmmsg(2) failure after earlier frames went out). The frames before index were sent; the rest were not. |
BatchTooLarge | The batch is longer than [MAX_BATCH_FRAMES]. Nothing was sent. |
PartialSend | sendmmsg(2) took fewer frames than it was given (Linux): the first sent were sent, the rest not. The kernel drops the error of any frame after a call’s first, so resending the tail is how to learn it. |
Io | sendmmsg(2) returned -1 for the batch’s first frame (Linux); carries the underlying errno. Nothing was sent. |
WireError in core/pulse-wire-udp
Defined in core/pulse-wire-udp/src/wire.rs.
| Variant | Meaning |
|---|---|
BufferTooSmall | The buffer is shorter than a frame with no envelope, [HEADER_BYTES] + [TRAILER_BYTES]. |
InvalidEnvelopeLength | The frame declares an envelope longer than [MAX_ENVELOPE] or than the buffer holds. |
BadMagic | Magic mismatch — datagram is not a pulse-wire-udp frame. |
UnsupportedVersion | Version mismatch. |
CrcMismatch | CRC32C mismatch — wire-level bit-flip or mid-stream truncation; receivers count this toward loss metrics. |
EnvelopeTooLarge | The envelope is too large for one datagram, or, from a sender with FEC on, for its window’s parity to fit one; with fragmentation on, too large for a run of fragments. Refused before anything is sent, so the caller can chunk. |
InvalidFragment | A fragment that disagrees with itself: an index or count out of range, a total length over [crate::fragment::MAX_MESSAGE], or a sequence number that is not its run’s first plus its index ([crate::fragment::decode_fragment]). |
EnvelopeCodec | A decoded [Frame]’s envelope bytes are not a canonical [PulseEnvelope]. Returned only by [decode_with_envelope]. |
EnvelopeIdMismatch | The envelope decoded, but its content id is not the id of its type, name and payload: the frame passed its CRC, so the id is stale or forged. Returned only by [decode_with_envelope]. |
NegotiationError in pulse/mesh-capability
Defined in pulse/mesh-capability/src/lib.rs.
| Variant | Meaning |
|---|---|
TooShort | Buffer is shorter than [HELLO_BYTES]. |
MagicMismatch | First 4 bytes don’t match [HELLO_MAGIC]. |
UnsupportedVersion | Remote’s protocol version is newer than [MAX_VERSION]. |
ContextRegistryError in pulse/mesh-context
Defined in pulse/mesh-context/src/lib.rs.
| Variant | Meaning |
|---|---|
IdConflict | Two different entries share a context_id. |
Toml | TOML parse failure on [ContextRegistry::from_toml]. |
Json | JSON failure in [ContextRegistry::from_json] or [ContextRegistry::to_json]. |
LexRegistryError in pulse/mesh-lex
Defined in pulse/mesh-lex/src/registry.rs.
| Variant | Meaning |
|---|---|
Duplicate | Insert refused: an entry with the same (type_id, kind) is already registered. |
NameConflict | [LexRegistry::validate] found two entries with one name. |
EnvelopeCodecError in pulse/pulse-canon
Defined in pulse/pulse-canon/src/envelope.rs.
| Variant | Meaning |
|---|---|
BufferTooSmall | Buffer shorter than the fixed-header + minimum tail. |
Truncated | A length-prefixed field declared more bytes than buf contained. |
InvalidTypeNameTag | type_name presence tag was neither 0 (None) nor 1 (Some). |
InvalidTypeName | type_name bytes were present but not valid UTF-8. |
NestedCountExceedsBuffer | A nested payload declares more children than the remaining bytes can hold, at [MIN_CANONICAL_ENVELOPE] bytes each. |
NestingTooDeep | Nesting deeper than [MAX_NESTING_DEPTH], which the format itself does not limit. |
InvalidPayloadVariantTag | Payload variant tag was neither [PAYLOAD_VARIANT_RAW] nor [PAYLOAD_VARIANT_NESTED]. |
NestedDecode | A nested child failed to decode. |
UnknownFlags | The flags word sets a bit no [PulseEnvelopeFlags] flag defines. |
TrailingBytes | [PulseEnvelope::from_canonical_bytes] was given bytes after the envelope. |
SmokeError in tools/pulse-cli
Defined in tools/pulse-cli/src/ops/bench.rs.
| Variant | Meaning |
|---|---|
ReceiverSetup | The receiving socket could not be bound or configured. |
Sender | Sender-side failure (bind, connect, or per-pulse send). |
Recv | Receiving or decoding a pulse failed. |
SendThreadPanic | The sending thread panicked. |
RoundtripError in tools/pulse-cli
Defined in tools/pulse-cli/src/ops/codec.rs.
| Variant | Meaning |
|---|---|
DecodeFailed | The supplied envelope could not be re-decoded from its own canonical form. |
DecodeError in tools/pulse-cli
Defined in tools/pulse-cli/src/ops/codec.rs.
| Variant | Meaning |
|---|---|
Hex | Input was not valid hex. |
Codec | Decoded bytes did not parse as a canonical envelope. |
SendError in tools/pulse-cli
Defined in tools/pulse-cli/src/ops/udp.rs.
| Variant | Meaning |
|---|---|
Bind | Underlying socket bind failed. |
Send | Underlying sender failed. |
RecvError in tools/pulse-cli
Defined in tools/pulse-cli/src/ops/udp.rs.
| Variant | Meaning |
|---|---|
Bind | The socket could not be bound or configured. |
Recv | Underlying receiver failed. |
API Reference
Reference · where each API surface is documented and how to generate it
This book is a guided path; the crate- and binding-level API reference is generated straight from source, not duplicated into book chapters where it would drift. Each page in this section covers one surface:
- Rust: the workspace’s own crates, via rustdoc.
cargo doc --workspace --no-deps --openbuilds and opens it; every crate opens with an overview and every public item is documented, which the workspace’smissing_docslint enforces. The Rust client for the daemon is documented in the daemon’s book. - Python bindings: the
pulse_wirePyO3 module. - Node bindings: the napi-rs module.
- C bindings: the
cbindgen-generated header.
Every page below generates its reference locally from the code actually in this repository.
Why generate instead of write
Rustdoc and the FFI headers already carry the authoritative signatures, types, and per-item documentation the workspace’s missing_docs lint enforces (see Developer Onboarding). A hand-written API reference in this book would either duplicate that content and drift out of sync, or omit detail the generated reference already has for free. Generating it is also how the gate keeps the pages honest: a generated page that differs from its generator fails the docs check.
Python bindings
Reference · the Python binding: what it wraps, how to build it and where to read its API
The Python binding lives at bindings/python/ — a PyO3/maturin module named pulse_wire, wrapping Pulse, Sender, and Receiver over the reference UDP transport (see Language bindings for what it does and how it differs from the pure-Python thin client).
Build and inspect it locally:
cd bindings/python
maturin develop
python -c "import pulse_wire; help(pulse_wire)"
The module’s own docstrings are the source of truth for its Python-facing API. Its test suite (test_loopback.py, test_send_buffer.py, test_recv_into.py, test_canonical_codec_cross_language.py) is also a good place to see real call patterns.
Node bindings
Reference · the Node binding: what it wraps, how to build it and where to read its API
The Node binding lives at bindings/node/ — napi-rs bindings exposing Pulse, Sender (bind, connect, send, sendTo), Receiver (bind, setReadTimeoutMs, recv, recvInto), and the PULSE_WIRE_SIZE/MAX_ENVELOPE/HEADER_BYTES constants (see Language bindings).
Build and inspect it locally:
cd bindings/node
npm install
npm run build
node -e "console.log(Object.keys(require('./index.js')))"
The generated index.d.ts TypeScript declarations (produced by napi-rs from the Rust source) are the most direct API reference — they carry accurate signatures for every exported class and constant. __test__/smoke.test.mjs and canonical_codec_cross_language.test.mjs show real usage.
C bindings
Reference · the C binding: what it wraps, how to build it and where to read its API
The C binding lives at bindings/c/ — a cdylib/staticlib FFI layer over pulse-wire-udp (see Language bindings). build.rs runs cbindgen to regenerate the header from the Rust source, which is the actual API reference:
cd bindings/c
cargo build --release
cat include/pulse_wire.h
The header documents pms_sender_bind/pms_receiver_bind (caller-owned handles), pms_sender_send / pms_sender_send_to / pms_receiver_recv_into, the matching pms_*_destroy calls, and the pms_status error enum plus thread-local pms_last_error(). tests/smoke.c and tests/run.sh show a minimal working call sequence end to end.
Language bindings
Reference · the codec bindings and the thin clients in the tree, by kind
Pulse Mesh offers two different ways to use it from outside Rust, depending on whether you want to embed the codec/transport natively or just talk to a running daemon.
Native bindings (compiled)
These wrap the wire codec and UDP transport directly, for embedding in another language’s runtime. None are Cargo workspace members — build each from its own directory.
bindings/c/— a C FFI (cdylib/staticlib) overpulse-wire-udp; see C bindings.bindings/python/— a PyO3/maturin module namedpulse_wire; see Python bindings.bindings/node/— napi-rs bindings; see Node bindings.
Build these with maturin/napi/the C toolchain plus cbindgen, from within their own directories — mixing their linker settings into the workspace-wide cargo build --workspace is exactly what the workspace exclusion avoids.
Thin clients (pure-language, no compilation)
These talk to an already-running mesh-hub daemon over WebSocket — no native compilation step, no linking against the Rust codec:
clients/pms-py-thin/(daemon repository) — Python packagepms, exposingMesh/MeshEvent/attach.clients/pms-node-thin/(daemon repository) — a single-dependency (ws) Node package, classMesh.
Both discover the daemon via the PMS_DAEMON environment variable, defaulting to ws://127.0.0.1:9876; neither supports the Unix-domain-socket transport, only WebSocket. If you’re prototyping a producer or consumer in Python or Node and don’t need the raw codec, start here rather than with the native bindings — Two programs talk through the daemon runs both.
WebAssembly
bindings/wasm/ compiles the canonical codec (pulse-canon) to WebAssembly with wasm-bindgen. It exposes encode, decode, verify and content-address for envelopes, decode and build for 128-byte Pulses, and check_fixture, which re-runs a conformance fixture inside the browser. Build with ./build.sh (the installed wasm-bindgen CLI must match the version pinned in its Cargo.toml); node tests/smoke.mjs verifies the build against every fixture under pulse/pulse-spec/test-vectors/. The Pulse Mesh website consumes this build for its in-page encoder and live visualizations.
Glossary
Reference · for every reader; the book, the website, the command line and the error messages use these words in these senses
Terms are listed alphabetically. Where a word has more than one meaning in this tree, each meaning has its own entry and the book always says which one it means. The Sovereignty stack’s names are included because Pulse Mesh sits beside them; where a component is not part of this repository the entry says so.
ACK — A 12-byte feedback datagram (magic plus a u64 cursor) the UDP receiver sends to the sender to report its cursor, one past the highest sequence it has seen. The sender’s backpressure stall is released by it. Defined in pulse-wire-udp.
Aegis™ — The bounded, typed execution layer of the Sovereignty stack (Aegis / Construct). Not part of this repository.
Attach — The first operation on a port-protocol connection: the client names itself, announces the protocol version and the capabilities it wants, and receives its node id.
Backpressure — The feedback loop that stops a UDP sender from running more than target_queue_depth frames ahead of the receiver’s ACK cursor, for at most stall_timeout before the send fails. Amber in every figure.
Basket — The SQLite capture store of envelopes that the daemon writes for structured queries (mesh-basket; pms and the MCP server query it). Distinct from the envelope store, which is shared memory.
BLAKE3-16 — The first 16 bytes of a BLAKE3 hash computed under a domain tag. The envelope id is one.
Busy claim — A store writer’s claim that found its slot held by another writer, mid-store or crashed, or already taken by a newer lap, and gave up its cursor for a fresh one. Counted in the store’s header and reported by busy_claims(); readers skip the cursors given up.
Canon™ — The canonical codec, pulse-canon: the crate that turns a Pulse and a PulseEnvelope into bytes and back, exactly the same way in every implementation. Also the Sovereignty stack’s name for that semantic layer.
Capability handshake — The 48-byte Hello exchange in mesh-capability by which two nodes on plain UDP agree on a transport upgrade (NACK retransmission, FEC, and the higher-tier backends) from the intersection of their capability bits.
Channel — The routing key an envelope is published on through the daemon (pms push --channel, Mesh::send). Subscriptions filter on it, by exact name or glob.
Content id — See envelope id.
CRC trailer — A CRC32C over bytes 8 to 124 of a Pulse, stored in bytes 124 to 128 of its slot by publish_with_crc and checked by read_slot_verify_crc. pms-crc32c owns the computation.
Cursor — The ring’s monotonic write counter. The slot a write lands in is the cursor modulo the capacity.
Daemon — mesh-hub: the local process that terminates the port protocol over a Unix socket and a WebSocket and bridges every connection to the runtime hub. One of the three meanings of hub.
Detach — The port-protocol operation that releases a client’s name and closes its subscriptions cleanly.
Developer profile — A tag, one of hft, ai, robotics, telemetry and dev, that pms modules ls --for and pms doctor --for take to select optimization modules. It installs and enables nothing. Not the profiles repository. Each profile’s modules are listed in Modules and developer profiles.
Domain tag — A fixed string mixed into a hash so that identifiers from different contexts can never collide. The envelope id uses pulse-envelope-id/v2.
Envelope — See PulseEnvelope.
Envelope id — The 16-byte BLAKE3 hash of an envelope’s canonical bytes under its domain tag. Identical content produces the identical id in Rust, Python, Node, C and WebAssembly; verify() recomputes it.
Envelope reference — The sixteen-byte envelope_ref field of a Pulse: the content id of the PulseEnvelope it names, byte for byte, or all zeros when the Pulse carries none. It has been part of the Pulse since the first commit and chapter 2 of the specification defines it; Pulse::envelope_id() reads it as a PulseEnvelopeId and set_envelope_id writes it. The daemon’s runtime fills it on the signal Pulse it writes for each stored envelope, and a second process attached to the hub fetches the envelope by it.
Envelope store — The content-addressed shared-memory store that sits beside a ring (core-store), where a Pulse’s envelope_ref resolves to the envelope it names. Layout v2 carries a magic, a version, a claim counter and per-slot sequence words, and holds each envelope as its canonical bytes, verified when read. Emerald in every figure.
FEC — Forward error correction in pulse-wire-udp: parity datagrams over a window of frames, so a receiver can rebuild a lost frame without asking for it.
Fixture — A byte-exact test vector under pulse/pulse-spec/test-vectors. Every implementation of the codec must reproduce the fixtures byte for byte; the conformance pages and the website’s scrubber are built on them.
Four-crate ring — The shape every ring under rings/ has: <ring>-core (shared types), <ring>-functions (stateless primitives), <ring>-stages (stateful processing) and <ring>-hub (the composed surface a consumer depends on). A layer with nothing to hold stays as an empty crate so the shape is uniform.
Four-question test — The rule in CONTRIBUTING.md for where new code goes: whether it touches the canonical Pulse atom, touches the canonical envelope, crosses a ring boundary or sits on the hot path decides whether it belongs in the inner stack; anything else is peripheral.
Heartbeat overlay — The HeartbeatPayload that occupies the last 32 bytes of a Pulse whose instruction is Heartbeat, read and written through heartbeat_payload and set_heartbeat_payload.
Hermes — The orchestration surface of Sov OS. Not part of this repository.
Hub — Three things, and the book says which: the daemon (mesh-hub), the runtime event bus it wraps (PulseHub in mesh-hub-runtime, combining a ring, an envelope store, the schema and broadcast registries and the health registry), and a ring’s hub crate (<ring>-hub).
Instance identifier — Sixteen random bytes a hub draws when it is created or opened, so every handle on a hub name has its own. The hub writes it into an origin_id left unset on the Pulses it publishes and skips the envelope signals that carry it, since its own listeners already had those envelopes.
Instruction — The 16-bit code in a Pulse that says what it signals: Null, Heartbeat or Data in this tree.
Knox™ — Cryptographic assertion in the Sovereignty stack (Knox / Musashi). Not part of this repository.
Kronos™ — The Sovereignty stack’s temporal authority: time as a load-bearing coordinate. The monotonic timestamp every Pulse carries and the hub’s temporal validation mode are the parts of that discipline present here.
Lane — One ring of a MultiLane set (hft-stages), named <prefix>-l<index>, so several writers can run in parallel without sharing a slot.
Lap — One pass of a ring’s or a store’s write cursor around its slots: cursor / capacity. The store’s sequence words encode the lap (2·lap + 1 in progress, 2·lap + 2 published), so a reader can tell that a slot holds the cursor it asked for and not the same slot from an earlier pass. The ring’s slot counter serves a poller the same way: each completed write adds two, so a copy whose counter shows more writes than its position’s lap needs may hold the next lap and is counted lost.
Layout version — The number a shared-memory header carries for its own layout: PMES version 2 for the envelope store, PMSR version 1 for the ring header. An open refuses a region whose version is not its own (LayoutMismatch). The ring’s header version numbers the header, not the slot format, which is version 1 at 128 bytes or version 2 at 192.
Lexicon™ — The conformance registry and the Merkle root over its public entries (mesh-lex), historically written R-LEX. The lex root anchors the published catalog; the daemon logs it at start.
Mesh — The set of processes attached to one hub, and by extension the substrate they share.
Monotonic nanoseconds — The monotonic_nanos field of a Pulse: a clock reading that must not go backwards within a hub, which the temporal validation mode enforces on the Pulses an application publishes. A hub stamps its own signals with the host’s monotonic clock (CLOCK_MONOTONIC on Linux, CLOCK_UPTIME_RAW on macOS) unless the application drives its clock.
MultiLane — N rings striped for parallel writers in the HFT ring, with NUMA placement and per-lane writer pinning.
Musashi™ — Proof-gated execution in the Sovereignty stack: authorization as witnessed mathematics. Not part of this repository.
NACK — A 12-byte feedback datagram asking the sender to retransmit one sequence number from its send cache. The receiver stages each missing sequence once, emits at most sixteen per cycle, repeats a request after four cycles and gives up after three; the sender bounds its queue and its retransmits per send.
Node id — The 16-byte identity the daemon derives from a client’s attach name (NodeId::from_name), rendered as name@hex.
Optimization module — One of twelve capabilities, M01 to M12, each a crate of its own or a module inside one layer of a ring, tagged with the developer profiles it suits. pms modules ls lists them, from a static table in the dev ring.
Origin — The producer’s node id as carried on an envelope event and matched by subscription filters.
Owl™ — Observation and telemetry in the Sovereignty stack. Not part of this repository.
Port protocol — See Universal Port Protocol.
Profiles (repository) — pulse-mesh-profiles, the repository of the enterprise, HFT and robotics rings, composed on the standard. Not a developer profile.
Pulse — The 128-byte atom: eleven fields in two cache lines, the first 8 bytes being the SeqLock word of the slot it sits in. Cyan in every figure.
Pulse Mesh™ — The wire format and the shared-memory transport this book documents, and the substrate the Sovereignty stack moves through.
PulseEnvelope™ — The variable-size, content-addressed envelope: a header, a payload that is raw bytes or a nested list of envelopes, and the envelope id computed over the canonical bytes. Violet in every figure.
PulseRegion — A shared-memory region holding exactly one Pulse under the SeqLock protocol (core-region).
PulseRing — The SeqLock ring in core-ring: a header and N slots in POSIX shared memory, one writer, whose publishes take its handle’s &mut, and any number of readers, each through a handle of its own in the writer’s process. The header records the ring’s magic, header version, capacity and slot size, and open refuses a mismatch. Version 1 slots are 128 bytes; version 2 slots are 192 bytes and add a ready line.
Ready line — The 8-byte word at bytes 128 to 136 of a version-2 slot that the writer bumps after a publish, so a hardware-wait receiver can monitor it instead of the SeqLock word. A reader may also poll it in place of the counter (PulseRing::read_position_by_ready).
Recovery window — The range of sequence numbers a UDP receiver may still ask the sender to retransmit: the sender’s send-cache capacity, 1,024 frames by default. A gap wider than the window keeps only its newest window of sequences staged; the rest are given up and counted.
Ring (buffer) — A PulseRing. When the book means the data structure it says ring buffer or PulseRing.
Ring (domain surface) — One of the four tier-3 surfaces: the dev ring, in the daemon repository, and the enterprise, HFT and robotics rings, in the profiles repository. When the book means these it says the dev ring, the HFT ring, and so on.
RITA™ — Modular role inference in the Sovereignty stack: governance mathematics as a replaceable part. Not part of this repository.
Sage™ — Deterministic governance in the Sovereignty stack. Not part of this repository.
Satellite — An external repository that implements a binding or an adapter against the conformance fixtures. modules.toml, in the profiles repository, defines the fields such a repository registers with; it lists none today.
SeqLock — The sequence-counter protocol that protects a slot: the writer marks the counter odd, copies the payload, and marks it even; a reader that sees an odd counter, or a counter that changed across its copy, retries for a bounded number of spins and then gives up.
Slot — One Pulse-sized cell of a ring, addressed by cursor modulo capacity.
Sov OS™ — The integrated experimental substrate of the Sovereignty stack, also written sovOS. The hub runtime and the module hub in this tree were vendored from its mesh core.
Stage — See four-crate ring.
Stall deadline — How long a UDP send may wait for the receiver’s cursor under backpressure before it fails with SenderError::Stalled: stall_timeout, five seconds by default, None for no deadline.
Subscription — A server-pushed stream of events matching a filter, opened by subscribe and identified by the id the ack carries. A tap is a subscription opened from the command line or Mesh::tap.
Thin client — The Python and Node clients in the daemon repository (clients/) that speak the port protocol directly, without the Rust client.
Tier — One of the four dependency layers: pulse (0), core (1), mesh (2), rings (3). A tier depends only on the tiers below it; the peripheral surface sits outside them and depends on all of them.
Type id, type name — The numeric and the named envelope type. A name-routed envelope carries its name, and its type id is 0 unless the sender gave one; a schema registered for a type id other than 0 checks it, named or not. Filters may match either.
Universal Port Protocol — Version 0 of the JSON protocol between the daemon and its clients: the operations attach, detach, send, subscribe, unsubscribe and health, and the replies ack, error, event and health, over a WebSocket (subprotocol pms-universal-port-v0) or a length-prefixed Unix socket. The types live in mesh-port-protocol.
WISP™ — The Sovereignty stack’s identity primitive. Not part of this repository; the registry at sovfound.org/registry records the mark and its status.
Why it is shaped this way
Why it is shaped this way
One claim, the mechanism that makes it true, and what would refute it.
Explanation · for anyone who wants to understand the design before trusting it
An explanation page makes one claim about the design, shows the mechanism that makes it true, and says what would refute it. It does not give steps and it does not list facts for lookup; those belong in the other parts. This opener carries the first explanation, the shape of the whole system, because every later page assumes it.
- Why a 128-byte atom, Why content addressing: the two primitives.
- The SeqLock memory model: the substrate; Substrate versus contract, in the daemon’s book, is where the substrate ends and the daemon’s contract begins.
- Transport design: moving bytes between machines; Daemon design and Trust boundaries, in the daemon’s book, are moving them between processes.
- Decision log: what was decided.
The shape of the system
The system is four tiers, and a tier depends only on the tiers below it. This repository holds the bottom two; the other two are repositories built on it.
| Tier | Where | What lives there | Color in every figure |
|---|---|---|---|
| 0 | pulse/, here | The canonical primitive: the Pulse, the PulseEnvelope, the codec that turns them into bytes, the conformance artifacts that pin those bytes, and the vocabulary (the Lexicon, the context and capability registries). | Cyan for the Pulse, violet for the envelope |
| 1 | core/, here | The engine: the SeqLock ring, the envelope store, the single-slot region, the reference UDP transport, the transport trait, the backoff and CRC primitives, and the macro the rings compose with. | Emerald for the substrate, amber for transport |
| 2 | mesh/, in the daemon | The fabric: the daemon and its port protocol, the embedded hub runtime, the Rust client, broadcast, health, the envelope basket. | Emerald and amber, by what each crate touches |
| 3 | the dev ring in the daemon; rings/ in the profiles | Domain surfaces composed over the fabric: a dev ring, an enterprise ring, an HFT ring and a robotics ring. | Slate for what a figure excludes |
Beside the tiers, each repository has its leaves: here the pulse command line, the bindings, the examples and the fuzz harness; in the daemon the pms tools, the thin clients and the basket query tool; in the profiles the XDP plugin and the HFT telemetry tool. They consume the tiers and are consumed by nothing.
Every ring has the same four crates, and the shape is deliberate. <ring>-core holds the shared types the ring’s other layers reuse, with no logic and no I/O. <ring>-functions holds stateless primitives. <ring>-stages holds the stateful processing: pipelines, lanes, delivery. <ring>-hub composes the three and is the only crate a downstream consumer depends on. A layer that has nothing to hold stays as an empty crate, so that every ring reads the same way. The macro that composes a hub, register_stages!, lives here in core/mesh-stage-registry, because it depends on nothing and every hub uses it.
The colors in the table are the website’s and the book’s: cyan is the Pulse, violet the envelope, emerald the substrate that holds them, amber the transport that moves them, rose an error or a mismatch, slate whatever a figure sets aside. A figure that uses them is self-describing once the legend is known.
What would refute this
The claim is that the tiers depend only downward and that every ring keeps the four-crate shape. cargo tree over each workspace is the check, and scripts/arch-check.py runs it in every repository’s gate: a tier-2 crate that depends on a tier-3 crate, a crate here that depends on the daemon or the profiles, or a ring whose hub is not the only entry point, contradicts the page.
Why a 128-byte atom
Explanation · for anyone deciding whether a fixed-size control signal is the right primitive, and why this size
The claim. A control signal that is exactly two cache lines, aligned to one, can be read with two memory fetches and no parsing, and can be moved through shared memory without a lock. Every other property of the Pulse follows from choosing that size and alignment before choosing the fields.
The mechanism
A processor moves memory in cache lines, 64 bytes on the x86-64 and AArch64 machines this tree targets. A structure that starts on a line boundary and is a whole number of lines long is fetched line by line with nothing wasted and nothing shared: no byte of it sits in a line that also holds a neighbour’s data. That last point is the one that matters for a ring. If two slots shared a line, a writer publishing into one slot would invalidate the line in every core reading the other, and the readers would pay for a write they never asked about. That is false sharing, and 64-byte alignment removes it by construction.
Two lines rather than one is a decision about what the signal must carry. The first line holds identity and routing: the SeqLock word the substrate owns, the 16-byte origin, the monotonic clock reading, the chain step, the instruction, the scope, and the 16-byte reference to an envelope. The second holds 32 bytes reserved for a replay defence that this release does not implement, and 32 bytes whose meaning follows the instruction: a heartbeat’s vitals, or, for a Data pulse, which stored copy of its envelope to fetch. A single line could not hold a content reference and an origin and a clock; three lines would carry padding on every hot path for a payload that belongs in an envelope anyway. Two is the smallest whole number of lines that carries a complete signal.
Fixed size is what makes the ring a ring. Slot i is at a fixed offset, so a reader needs no index and a writer needs no allocator; the cursor modulo the capacity is the whole addressing scheme. It is also what makes the SeqLock cheap: the counter is the first eight bytes of the slot, the copy is a constant 120 bytes, and the reader’s check is two loads and a compare.
What the size costs
A Pulse cannot carry a payload. Anything larger than its fields travels as a PulseEnvelope, referenced by content, which means a second read for any message with data. The design accepts that cost deliberately: the reference to the envelope is itself content-addressed, so the second read is a lookup by identity in a store where identical content is held once, and the Pulse stays a signal that a million-per-second control loop can afford.
What would refute this
The claim rests on a Pulse being 128 bytes and 64-byte aligned, which the codec asserts at compile time, and on false sharing being absent, which the ring’s torn-read test exercises with four readers against one writer. A measurement in which a reader’s latency rises when a writer publishes into an adjacent slot would contradict the alignment argument; a Pulse layout whose fields no longer fit two lines without a payload would contradict the size argument.
Spoken walkthrough
A script for the video and audio version of this page, about a minute.
A processor moves memory sixty-four bytes at a time, and that one fact decides the size of a Pulse. Make the signal exactly two of those lines, start it on a line boundary, and every read is two fetches with nothing wasted and nothing shared with a neighbour. Share a line between two slots and a writer in one slot slows every reader of the other. So: one hundred and twenty-eight bytes, aligned.
Why two lines and not one? The first holds who and where: the counter, the origin, the clock, the instruction, the scope, and a reference to an envelope. The second holds room for a replay defence this release does not yet implement, and thirty-two bytes that follow the instruction: the vitals a heartbeat carries, or where a data pulse’s envelope sits in the store. One line could not hold a reference and an origin and a clock; three would pay for padding on every message.
The cost is that a Pulse carries no content of its own. Anything bigger travels as an envelope, named by its content, fetched by that name. The Pulse stays a signal that a million-per-second loop can afford.
Why content addressing
Explanation · for anyone asking why an envelope is named by a hash of its bytes rather than by a sequence number or a sender-assigned id
The claim. Naming an envelope by the BLAKE3 hash of its canonical bytes gives every implementation, in every language, the same name for the same content, without coordination; and that one property is what makes cross-language byte equality checkable, storage deduplicating, and a nested envelope tamper-evident.
The mechanism
The identifier is computed from what the content is, not from who produced it or when: the envelope type, the type name if any, the payload variant and the payload bytes, each length-prefixed, under a fixed domain tag. Origin, timestamp and flags stay outside the preimage, so two producers that emit the same event get the same identifier and can be deduplicated, and a router that flips a flag does not change what the envelope is named. The domain tag is the part that keeps the name honest across the whole stack: a hash of the same bytes computed for some other purpose can never collide with an envelope identifier, because the tag is different.
Because the name is a function of the bytes, equality of names is equality of content. That is the test the conformance vectors run: an implementation that reproduces the vector’s identifier from the vector’s fields has reproduced the canonical bytes, byte for byte, because any difference in the bytes would change the hash.
The nested variant turns the property into provenance. A parent’s identifier covers its children’s canonical bytes, and each child’s canonical bytes begin with the child’s own identifier; change a leaf and every identifier above it changes. A batch of events, an audit chain of states, a broadcast of a bundle: each is one envelope whose single identifier vouches for everything inside it.
What it costs
A hash over the payload on every encode, and, when a receiver verifies, on every decode; the reference makes verification a separate call so a trusted shared-memory path can skip it and an untrusted network path cannot. Sixteen bytes of identifier instead of eight of sequence number. And a discipline: the canonical bytes must be exactly specified, because any freedom in the encoding would let two implementations agree on the content and disagree on the name.
What would refute this
Two conformant implementations producing different identifiers for the vectors’ inputs would refute the claim outright. So would a demonstrated collision at the 16-byte width for two distinct canonical byte strings, or an identifier that changed when only the origin, the timestamp or a flag changed.
Spoken walkthrough
A script for the video and audio version of this page, about a minute.
An envelope’s name is a hash of its bytes. Not a sequence number, not something the sender chose: the type, the type name, the payload, each with its length, under a fixed tag. The origin and the time stay out, so two producers of the same event produce the same name, and a router that flips a flag changes nothing about what the envelope is.
Because the name is a function of the bytes, equal names mean equal content. That is what the conformance vectors test: reproduce the identifier and you have reproduced the bytes, in any language.
Nest envelopes and the property becomes provenance: a parent’s name covers its children’s bytes, so change a leaf and every name above it changes. One identifier vouches for everything inside.
The price is a hash on every encode and, when you verify, on every decode; sixteen bytes of name instead of eight of sequence; and a canonical encoding with no freedom in it, because any freedom would let two implementations agree on the content and disagree on the name.
The SeqLock memory model
Explanation · for anyone who wants to know why a reader can trust a Pulse it copied out of shared memory while a writer was running
The claim. A reader that observes the slot’s sequence counter even and unchanged across its copy has a Pulse that one publish wrote in full, on any processor the tree targets, and a reader that cannot get such an observation within a bounded number of tries has learned that the slot is torn or abandoned, not that it should keep waiting.
The mechanism
The counter in a slot’s first eight bytes is a version number with a parity bit. A writer stores an odd value, copies the Pulse’s other 120 bytes, then stores the next even value. A reader loads the counter, copies the slot, loads the counter again, and keeps the copy only if both loads were the same even number. If a write started during the copy, the second load is odd or larger; if a write completed between the loads, the number moved. The reader retries in either case. The writer never waits for anyone: a slow or absent reader costs it nothing, which is why a publish is a fixed few nanoseconds regardless of how many readers there are.
That is the algorithm on paper. What makes it true on hardware is the pair of fences. A processor and a compiler are free to reorder a plain store or load with the atomic operations around it, so without a fence the payload copy could become visible before the odd mark, or the reader’s copy could be served after the second load. The writer therefore issues a release fence between the odd mark and the copy, and the reader an acquire fence between the copy and the second load. On x86 the hardware’s ordering hides the bug, which is how the tree ran for months without one; on AArch64 it does not, and the fences were added after a torn-read test written for this Mac showed mixed bytes in a “consistent” Pulse. The test drives four readers against a writer and asserts that no reader ever sees a Pulse whose fill bytes disagree.
The copy itself is atomic too. The writer stores the 120 bytes after the counter, and the reader loads the slot, as relaxed atomic stores and loads of eight-byte words (core_region::shm::store_words and load_words). The fences order the copy against the counter, but they cannot make a plain memory copy that races a write defined: in Rust that is undefined behaviour even when the reader throws the copy away, and no test can show it absent. With every shared access atomic, a racing write can only make a copy stale, which the second counter load catches. On x86-64 and AArch64 a relaxed load or store of a word is an ordinary move, so the atomics cost nothing the hardware was not already doing.
Crashed writers and bounded readers
A writer that dies between its two stores leaves the counter odd forever. Two rules keep that from becoming a hang. The reader’s retry is bounded, one hundred spins in the reference, after which it reports nothing: for that slot the right answer is “no consistent Pulse here”, not “wait longer”. And the next writer to reach the slot reads the counter with its low bit cleared before marking it odd, so an abandoned odd counter is re-based and the slot becomes readable again after the next publish. Before that rule the publish paths computed their marks from the odd value and left the slot unreadable for the life of the region.
The same protocol protects the envelope store’s slots and the single-slot region, and the SPMC ring in the HFT ring, with one addition in the store: the counter there encodes the lap, so a reader can tell that a slot holds the cursor it asked for and not the same slot from a previous pass around the ring; and a slot held by a writer that died there is not waited for, because the store’s writers route around it to a fresh cursor and its readers skip it at once. The SPMC ring and the multilane lanes read the counter for the same purpose: a reader that has fallen a capacity behind is moved to the oldest publish still held, a copy the writer may have overwritten with the next lap is discarded, and both are counted as lost rather than delivered under an old index or delivered twice. The hub runtime’s poll applies the same rule through the ring’s read_position, with the slot counter serving as the lap count: each completed write adds two, so a stable copy whose counter shows more writes than its position’s lap needs is discarded as lost.
What it costs
Two counter stores, a fence and fifteen word stores per publish; two counter loads, sixteen word loads, a fence and a compare per read; and the possibility of a retry under contention. A reader that needs a slot while a writer is mid-publish spends nanoseconds spinning rather than blocking. What it does not cost is a lock, a syscall, or a writer that stalls on a reader.
What would refute this
A reader returning a Pulse with bytes from two different publishes would refute the claim; the torn-read test is the check, and the fences are what make it pass on a weakly ordered processor. A reader that never returns from a slot whose writer died, or a slot that stays unreadable after a later publish, would refute the bounded-reader and crashed-writer rules; the crashed-writer tests are those checks.
Spoken walkthrough
A script for the video and audio version of this page, about ninety seconds.
How does a reader trust a Pulse it copied while a writer was running? With a counter and two fences.
The counter is the slot’s first eight bytes. A writer stores an odd number, copies the Pulse in, and stores the next even number. A reader loads the counter, copies the slot, loads the counter again, and keeps the copy only if both loads were the same even number. If a write began during the copy the second load is odd or larger; if one finished between the loads the number moved; either way the reader tries again. The writer never waits for anyone.
That is the algorithm. On hardware, two fences make it true: a release fence between the odd mark and the copy, an acquire fence between the copy and the second load, so neither the processor nor the compiler reorders them. On x86 you would never notice the fences missing. On ARM you would: a test written on an Apple-silicon Mac showed mixed bytes in a copy that the counter said was clean, until the fences went in.
Two rules keep a dead writer from hanging anyone: the reader gives up after a hundred tries and reports nothing, and the next writer re-bases an abandoned odd counter so the slot reads again after the next publish.
The cost: two counter stores, a fence and fifteen word stores to publish; two loads, sixteen loads, a fence and a compare to read. No lock, no syscall, no writer stalled on a reader.
Transport design: the feedback loop, retransmission and parity
Explanation · for anyone tuning the UDP transport or deciding which of its mechanisms a link needs
The claim. A fire-and-forget datagram transport becomes reliable enough for control traffic with three separable mechanisms, each of which costs something different: a feedback loop that keeps the sender from outrunning the receiver, a bounded retransmission path for the frames that are lost anyway, and a parity frame that recovers one loss per window without a round trip. None of them changes the frame a Pulse travels in.
The feedback loop
The receiver tells the sender, once per millisecond, how far the stream has reached it: one number, its cursor, one past the highest sequence it has seen. The sender keeps the highest such number it has seen and refuses to send while more than a fixed number of frames (sixteen by default) are in flight beyond it, backing off from spins into short sleeps until the number moves or a deadline passes, five seconds by default, when the send fails instead of waiting forever; a cursor ahead of the sender, left by a receiver that outlived a restart, counts as nothing in flight. A batch of frames is held to the same rule one frame at a time, so batching buys the syscall and nothing else. The loop needs no negotiation and no state on the wire beyond the sequence number every frame already carries, and because the sender only ever raises its cursor, a replayed or reordered acknowledgement is harmless. What the loop does not do is guarantee delivery: a lost frame simply leaves a gap.
Retransmission, bounded on both ends
The receiver names the gap: each missing sequence inside its recovery window is staged once, when the arrival that reveals it lands, and requested with a negative acknowledgement, at most sixteen per period, again after four periods if still missing, and at most three times in all; the sender answers from a cache of its last thousand encoded frames, byte for byte, to its own data destination. Every step is bounded because every step is an amplifier a peer or a fault could drive. A gap wider than the window keeps only its newest window staged; the receiver gives up the rest, counts them, moves past them and counts a resync, because a window the sender no longer holds cannot be recovered and staging it would only fill memory. The sender’s queue of pending requests is capped at the cache size, it drains at most sixteen per send, and a request for a frame outside the cache is a counted miss. A forged request cannot redirect traffic, because the retransmission goes where the data goes, and the feedback socket can be told which peer to listen to.
Parity instead of a round trip
On a long path the round trip a retransmission costs is the whole latency budget. Forward error correction spends bandwidth to save it: after every window of K frames the sender emits one parity frame, the exclusive or of the window, and a receiver that lost exactly one frame of the window rebuilds it from the other K − 1 and the parity, then decodes the rebuilt bytes through the same path as any frame, CRC included. Two losses in a window fall through to retransmission. K is the whole tuning knob: a small window recovers more often and costs more bandwidth; a large one costs less and recovers less. A window of zero is a configuration error and is refused.
The mechanisms added since
Four more sit beside the three, each opt-in and each leaving the frame alone. Congestion control sizes the backpressure window to the path: a window of frames in flight that grows while acknowledgements advance and halves on loss, with optional pacing across a round trip. Fragmentation sends an envelope up to MAX_MESSAGE (66,560 bytes) as a run of datagrams, which the receiver reassembles within a bound; switched on at both ends, it moves the per-datagram limit without touching the frame. Delivery options put a sequencer between the intake and the caller, for duplicate suppression (a sequence number can arrive twice with NACK or FEC on) and in-order delivery with a per-gap timeout. Multicast sends once to a group that every member receives. Sealing, ADR-0018’s authenticated and encrypted datagrams, wraps whatever the transport would have sent, fragments and feedback included, and is described under Trust boundaries; its review is pending.
Choosing
Each mechanism is switched on by configuration, on both ends where it needs both. The capability Hello that lets two peers announce what they can do exists, and the daemon’s links exchange it inside their sealed session; on plain UDP nothing sends it yet, and nothing acts on an agreed upgrade yet, so choosing is still the operator’s. A local link needs none of these; a lossy link with a short round trip wants retransmission; a long, lossy link wants parity in front of retransmission; any link where the receiver is slower than the sender wants the feedback loop first, with congestion control when the path, not the receiver, is the limit; and a message larger than a datagram wants fragmentation at both ends.
What would refute this
A sender that keeps sending past the receiver’s cursor, a retransmission that goes to the address a request came from, an unbounded queue of requests, a window with one loss that is not recovered, or a parity frame that decodes to bytes the CRC does not accept would each refute a claim above. The transport’s backpressure, retransmission, gap-bound, queue-bound, feedback-filter and parity tests are the checks, and each runs over loopback in the workspace test suite.
Spoken walkthrough
A script for the video and audio version of this page, about ninety seconds.
UDP forgets. The transport makes it remember enough for control traffic, with mechanisms you switch on one at a time, none of which changes the frame a Pulse travels in.
First, the feedback loop. Once a millisecond the receiver tells the sender how far the stream has reached it, one number, and the sender refuses to run more than a few frames past it. No negotiation, no new state on the wire.
Second, retransmission. The receiver names the frames it is missing, a bounded number at a time, a bounded number of times, and the sender answers from a bounded cache. Every step is bounded, because every step is something a peer or a fault could drive.
Third, parity. On a long path the round trip is the whole budget, so after every window of frames the sender adds one parity frame, and a receiver that lost one frame rebuilds it without asking.
Since then: a congestion window on the feedback, fragmentation for envelopes larger than a datagram, in-order and duplicate-free delivery, multicast, and sealing, which is authenticated encryption awaiting its independent review.
A local link needs none of these. A lossy link wants retransmission. A long lossy link wants parity in front of it. A slow receiver wants the feedback loop first.
Decision log
Explanation · generated · the architectural decision record under `decisions/`, in order, never rewritten
Each record states its context, the decision and its consequences, and stays in place when a later record supersedes it. The index below is read from the files; the files are the record.
A non-obvious call (where something belongs, why a crate is shaped the way it is, anything a later contributor would otherwise reverse-engineer from a pull request) gets a new record rather than only an explanation in the pull request. Number it one past the highest present, and say in its body which earlier record it supersedes, if any. Numbers are never reused, so a gap is a retired record.
| ADR | Title | Status | Date |
|---|---|---|---|
| ADR-0008 | Envelope store layout version 1 | Superseded in part by ADR-0011 (2026-09-24) | 2026-09-24 |
| ADR-0011 | Envelope store layout version 2 | Accepted | 2026-09-24 |
| ADR-0013 | One shared-memory mechanism | Accepted | 2026-09-25 |
| ADR-0014 | Pre-integration crates say so | Accepted | 2026-09-25 |
| ADR-0015 | Ring header layout | Accepted | 2026-09-24 |
| ADR-0016 | Gated rings: one store per publish, one check per read | Accepted | 2026-10-04 |
| ADR-0017 | Partial crates say so, and the architecture check holds them to it | Accepted | 2026-10-17 |
| ADR-0018 | The security model: what is protected, by what, and what is not | Accepted | 2026-10-17 |
| ADR-0019 | A reader table for lossless fan-out | Accepted | 2026-10-17 |
| ADR-0020 | An index of recent cursors for the envelope store | Accepted | 2026-10-17 |
Changing the tree
Changing the tree
The workspace, the gates, the decision record, and the rules this book holds itself to.
Part opener · for people changing code or documentation
Contributions go through the same gates whether they change code or prose: formatting, lints, the tests, the rustdoc link check, and for the book the structural checks that keep every page reachable, every link resolving, every displayed version equal to the workspace version, and internal process language out of released text.
- Developer Onboarding: the workspace, the branch and PR flow, the gates.
- Documentation style: the page kinds, the voice, and the review checklist for anything written.
- Decision log: the decision record under
decisions/and how to add to it.
Documentation style
Contributing · for anyone writing or reviewing a page of this book, a README, a crate overview or a message a user will read
The book is held to the same standard as the code: every page has one job, states it, and can be checked. This page is the standard. The review checklist at the end is what a documentation pull request is read against.
One page, one kind
Every page is exactly one of six kinds and says which in its first line, in the label the theme renders from <p class="pm-kind"><b>Kind</b> · for whom</p>.
| Kind | Job | Shape |
|---|---|---|
| Example | Show it working | Opens with what the reader will have and what they need; one self-contained prompt for a coding agent; the same steps by hand with the literal output of each; a table of checks with pass conditions and what failures mean; what happened, including what the code does not do; variations; a spoken script. |
| Workflow | Do one domain’s job end to end | An example’s shape, for one domain: robotics, enterprise, HFT or distributed compute. States which parts of the path are implemented and which are deferred. |
| Tutorial | Teach by doing | States what it assumes and what the reader will have at the end; numbered steps; every command run by the documentation gate; ends with what was learned. |
| How-to guide | Get a task done | Names the task, the prerequisites, the steps in order with the expected result after each; links out for background and facts; no teaching. |
| Reference | State what is true | Complete, precise, generated from the code where a table can be; no persuasion, no steps. |
| Explanation | Say why | One claim, the mechanism that makes it true, one figure, and a closing section on what would refute it. |
A page that wants to do two jobs is two pages.
Voice
The voice is the Foundation’s, as it stands on the Foundation’s website.
- Declarative and precise. State the claim, then the evidence. “A reader that sees an odd counter retries” rather than “readers will generally want to retry”.
- One idea per sentence. Short sentences carry weight; a sentence that needs a semicolon is two sentences.
- Institutional, not personal. No “we think”, no “I”, no “the author”. Tutorials and guides address the reader as “you”; reference and explanation pages address no one.
- No superlatives, no hedging, no marketing. “The ring publishes without a system call” rather than “blazingly fast”.
- Define a term before using it, and use the glossary’s word for it every time. Pulse, PulseEnvelope, the daemon,
pms, the four tiers, the dev ring: never a synonym. - Numbers carry units and a source. A measurement names the command that produced it.
- What is not yet true is said plainly. A specified behaviour that the code does not implement yet is named as such, with the test that will prove it.
- Superseded material stays in place and is marked; nothing is silently rewritten. Decisions go to
decisions/as append-only records. - Every explanation ends with what would refute it. Refutations are welcome and the page says so.
- American spelling. Present tense. Second person only in tutorials and guides.
Names that are welcome and names that are not
The Sovereignty stack’s names are public and are used deliberately: Canon, the Lexicon, Kronos, Sov OS, Musashi, Knox, Aegis, Sage, RITA, Hermes, Owl, WISP. The glossary maps each to the component that carries it here or says it is not part of this repository.
The names the registry lists as marks (Pulse Mesh™, PulseEnvelope™, Canon™, Lexicon™, Kronos™ and the rest; the list is at sovfound.org/registry) carry ™ on their first prominent use on a page: the hero, a part opener, a glossary entry, the first mention in prose. Later mentions on the same page do not repeat it.
What released text does not carry is the language of how the tree was made rather than what it is: internal phase labels, migration process notes, and names of documents or features that no longer exist. scripts/docs/lint-terms.py holds the list and runs in the documentation gate.
Structure
- Headings are phrased as the reader’s question or task in tutorials and guides (“Publish an envelope”, “What you will need”), and as the noun they describe in reference and explanation (“Slot layout”, “Why two reads”).
- Code comes before prose in a how-to guide: the command, then what it does.
- A page stays under about 1,500 words. A page that grows past it splits along its headings.
- Rust samples are included from the examples with
{{#include}}anchors rather than typed into the page, so the sample and the page cannot drift. Shell transcripts show the command and the output the reader should see. - Every reference table that can be generated from the code is generated, and the gate regenerates it and fails on a difference.
- One figure per explanation page, from the website’s catalogue where one exists, with a caption in the label idiom (
<figure class="pm-figure">). Colors mean what the legend says: cyan the Pulse, violet the envelope, emerald the substrate, amber the transport, rose an error, slate what is set aside. - Links are relative and point at pages of this book, at files in the tree, or at the decision record. The gate checks that every link resolves and every page is reachable from the summary.
Markdown conventions
One paragraph is one line; prose is never hard-wrapped at a column. List items and table rows are one line each. Code blocks name their language. Files begin with a single # heading and then the page kind line (part openers and the introduction carry their heading inside the hero block, with the kind line after it).
The review checklist
A documentation pull request is read against these questions, and each one is answered before it merges.
- Is the page exactly one kind, and does its first line say which and for whom?
- Does every command in it run in the documentation gate, and does the output shown match?
- Is every fact traceable to code, a test, a fixture, or a bench command named on the page?
- Does an explanation page make one claim, show one figure, and say what would refute it?
- Are all terms the glossary’s terms, used in the glossary’s senses?
- Does the page carry no internal process language, no persona, and no name of something that does not exist?
- Do the version, the links and the summary pass the gate (
just docs)? - Is anything superseded marked rather than deleted, and is any decision it records in
decisions/? - Is it under the length limit, or split?
- Would a reader who knows nothing of how this tree was made understand every sentence?
- Is every claim about what the code does, or lacks, checked against the code itself rather than against a comment, an earlier page or a review? A comment can be wrong from the day it was written, and a claim that is only ever checked against other claims is never refuted.