Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Sovereignty Foundation Inc. · The Pulse Mesh™ Book

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

Nothing in this book is a release announcement. Licensing questions go to legal@sovfound.org.

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

PartThe question it answersHow to read it
ExamplesHow 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.
WorkflowsHow 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 guidesHow do I do this particular task?Find the task, follow the steps.
ReferenceWhat exactly is true?Look things up. The specification and the generated tables live here.
ConceptsWhy 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

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-mesh is the standard; pulse-mesh-daemon and pulse-mesh-profiles build on it and find it at ../pulse-mesh.
  • AGENTS.md in 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.sh in 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

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

  1. Set up. rust-toolchain.toml pins the Rust version, and rustup installs it on the first cargo command.
  2. See it work.
    cargo install --path tools/pulse-cli --locked
    pulse version
    
    pulse version prints 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.
  3. Encode one envelope. Across a network encodes one, decodes it, shows the check fail on one changed byte, and then sends Pulses over UDP.
  4. Prove the tree. scripts/gate.sh --quick runs the per-commit checks; scripts/gate.sh runs 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.md says 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.md lists the rules every change keeps, for a person as much as for an agent: never edit a test vector to pass a test, unsafe only 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-canon and core/pulse-wire-udp: the two layouts, their codec, and the wire that carries them.
  • core/core-ring/src/lib.rs, the SeqLock ring, and core/core-store/src/lib.rs beside it, the envelope store.
  • tools/pulse-cli/src/main.rs and src/ops/: the pulse command 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.

  1. 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.
  2. 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.
  3. 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

senderreceivermagicseqpulseenvelopecrcone datagram, checked on arrival
One Pulse and its envelope per datagram

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

CheckCommandPass
The codec agrees with the specificationpulse versionThe codec-test-vector line ends 57f2c2eec9bae1b5e69c97f88e9bfd8d
Encoding is canonicalpulse encode --json spec.jsoncontent_hash 71aad243e722c17b9f1cf2723ec6148a, decoded_equal true
Tampering is caughtpulse decode of the hex with …68 changed to …69verified false, exit status 1
Datagrams arrive and verifypulse recv … --count 3 and pulse send … --count 3received 3/3, three seq= lines
The program agreescargo run --release -p example-02-loopback-udp-rustA 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 version checks.
  • 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 1000 on pulse send; the transport refuses an envelope larger than a datagram carries unless fragmentation is on.
  • A nested envelope: pulse encode with "envelope_type": 2 and 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.1 for a peer’s address and you have a sender on one machine and a receiver on another.

Next

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

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

ring, 64 slotswriterreaderstorethe pulse carries the envelope's name
The ring carries the signal, the store holds the content

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

CheckCommandPass
A Pulse and its envelope crossed to a second handle and verifiedcargo run --release -p example-03-embed-ring-rustThe OK — line with b6a5ee1e601cf94a3bd3ba244ddc0ee8
A mismatched open is an error, not a faultThe edges programring header capacity mismatch: the region has 64, the caller expects 32, then 64 from open_existing
Identity is contentThe edges programsame id: true; the never-stored envelope fetches None
The creator owns the namesThe edges programshm_open failed: errno 2 after drop(ring)
The guarantees holdcargo test -p core-ring --release, cargo test -p core-store --releaseEvery 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 open refuses a header that disagrees with what the caller expects; open_existing takes 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_existing and EnvelopeStore::open_existing (or open(name, capacity)) and reads; a reader must not create_or_open the 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 and publish_blocking keeps 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

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

Part 02 · How-to guides

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.

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-canon for another runtime. The Python, Node and C bindings under peripheral/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

FieldMeaning
pulses_sentPulses the sender sent.
pulses_receivedPulses the receiver decoded.
payload_bytesThe envelope size of every Pulse, 64.
send_elapsed_nsWall time the sender spent sending, in nanoseconds.
recv_elapsed_nsNanoseconds from the receiver’s first read to its last decode. The timeout that ends a short run is not counted.
throughput_pulses_per_secpulses_received per second of recv_elapsed_ns.
p50_ns, p99_ns, p999_nsOne-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

Part 03 · Reference

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

Reference · The Pulse Mesh Standard

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.

ChapterSubjectNormative for
1. IntroductionScope and terminologyEveryone
2. The PulseThe 128-byte atom: layout, alignment, instructions, the heartbeat overlayEvery implementation
3. The PulseEnvelopeThe canonical bytes, the content id, payload variants, recursion, self-verificationEvery implementation
4. The SeqLock ringThe shared-memory ring: header, slots, writer and reader protocol, crashed writersShared-memory substrates
5. The envelope storeThe content-addressed store beside the ring: layout v2, claims that route around held slots, verified reads, lappingThe reference implementation
6. The UDP transportDatagram framing, CRC, feedback, FEC, the capability handshakeThe reference transport
7. The Universal Port ProtocolThe daemon’s JSON protocol over WebSocket and Unix socket, and its connection policyEvery client of the daemon
8. ConformanceWhat a conformant implementation reproduces, and what changes require a draft revisionEveryone
Appendix A. BLAKE3The hash the content id is built onInformative

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.

OffsetSizeFieldTypeSemantics
08seq_locku64, atomicThe 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.
816origin_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.
248monotonic_nanosu64A 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.
328chain_stepu64Remaining steps in the optional hash chain; a countdown to zero is a time to live. Zero when the replay defence is not used.
402instructionu16The opcode, from the table below.
422_pad0u16Alignment padding. MUST be zero.
444scopeu32The execution scope bound; interpreted as a context id and a subscope by the context registry.
4816envelope_ref[u8; 16]The content id of an associated PulseEnvelope, or all zeros when none is attached.
6432preimage[u8; 32]The hash-chain preimage for the optional replay defence. MAY be zero.
9632_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

OpcodeNameMeaning
0x0000NullA blank slot: zeroed memory or an empty buffer.
0x0001HeartbeatA liveness signal. Bytes 96 to 128 carry the heartbeat overlay (2.5).
0x0010DataA 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 _pad2SizeFieldTypeMeaning
08schema_versionu64An opaque schema or configuration version, compared bytewise.
88uptime_msu64Milliseconds since the node started.
164error_countu32Errors since the node started.
202stage_countu16Registered processing stages.
222path_countu16Registered processing paths.
242consecutive_errorsu16Errors in the most recent window.
261wake_stateu8Position in the wake cycle, 0 to 15.
271health_stateu80 healthy, 1 warning, 2 restarting, 3 isolated.
284_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 _pad2SizeFieldTypeMeaning
08store_cursoru64The cursor at which the envelope store holds the copy, when has_store_cursor is 1.
81has_store_cursoru81 when store_cursor names a copy; any other value names none.
923reserved[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

OffsetSizeFieldTypeNotes
016id[u8; 16]The content identifier (3.2).
1616origin[u8; 16]The producing node. Not part of the identifier, so the same content from two origins shares one id. All zeros when unset.
328monotonic_nanosu64The producer’s creation time. Not part of the identifier.
402envelope_typeu16The numeric type. Part of the identifier.
422flagsu16Routing and behaviour flags (below). Not part of the identifier.

Type-name section

OffsetSizeFieldTypeNotes
441type_name_presentu80 absent, 1 present. Any other value MUST be rejected (the reference reports InvalidTypeNameTag).
454type_name_lenu32Present only when the tag is 1: the byte length L of the name.
49Ltype_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

OffsetSizeFieldTypeNotes
T1payload_variantu80 raw, 1 nested. Any other value MUST be rejected (InvalidPayloadVariantTag).
T + 14payload_len or nested_countu32The byte length M of a raw payload, or the number N of nested envelopes.
T + 5M or …payloadbytesM opaque bytes, or N envelopes each encoded by this same rule, concatenated.

Flags at offset 42:

BitMaskNameMeaning
00x0001BROADCASTFan out to every listener rather than point to point.
10x0002ACK_REQUIREDThe sender expects an acknowledgement.
20x0004PRIORITYSchedule with elevated priority.
30x0008COMPRESSEDThe payload is compressed; the codec is outside this layer.
4 to 15reservedEncoders 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:

  1. the domain tag, the ASCII bytes of pulse-envelope-id/v2, with no terminator;
  2. envelope_type as two little-endian bytes;
  3. the type-name section exactly as it appears on the wire: the byte 0, or the byte 1 followed by the four-byte length and the name bytes;
  4. the payload variant tag as one byte;
  5. the length of the payload bytes as four little-endian bytes;
  6. 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:

LimitWhat it isWhere it lives
1,315 bytesThe 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 bytesThe 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 capThe 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

BytesContents
0 to 8write_cursor, atomic u64: the number of publishes so far; the next publish lands in slot write_cursor mod capacity.
8 to 16total_writes, atomic u64: the count readers poll for new data.
16 to 24read_cursor, atomic u64: the slowest reader’s position, consulted only by the blocking publish mode.
24 to 28The magic PMSR, stored last when the ring is formatted, with release ordering.
28 to 32The header layout version, 1; it numbers the header, not the slot format.
32 to 40The capacity in slots, u64.
40 to 44The slot size in bytes: 128, or 192 for version-2 slots.
44 to 64Reserved, zero; the header is one cache line.
64 onwardcapacity 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:

  1. 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;
  2. store the odd value (the even base plus one) with release ordering, marking the write in progress;
  3. issue a release fence, so the mark is visible before any payload byte;
  4. copy the Pulse’s bytes 8 to 128 into the slot, leaving the counter word alone;
  5. store the even value (the base plus two) with release ordering, marking the slot stable;
  6. 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:

  1. load the counter with acquire ordering; if it is odd, a write is in progress: retry;
  2. copy the slot’s bytes out;
  3. issue an acquire fence, so the copy completes before the re-read;
  4. load the counter again; if it differs from the first load, the slot changed under the copy: retry;
  5. if the counter is zero, the slot has never been written: report nothing;
  6. 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

BytesContents
0 to 8The write cursor, atomic u64: a claim counter that hands out cursors and proves nothing about contents.
8 to 12The magic PMES, stored last when the region is created, with release ordering.
12 to 16The layout version, 2.
16 to 20The capacity in slots.
20 to 24The maximum entry size in bytes, 65,536 in this build.
24 to 32Busy claims, atomic u64: claims that found their slot held or taken and gave up their cursor.
32 to 64Reserved, zero.
64 onwardcapacity 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:

  1. encodes it canonically and refuses a body larger than the maximum entry size (EnvelopeTooLarge, naming the canonical size) before claiming anything;
  2. claims a cursor by incrementing the write cursor;
  3. claims the slot for that cursor by moving its sequence word from an even value no newer than 2·lap to 2·lap + 1 with a compare-and-swap; a claim never starts from an odd word, so no writer ever writes a slot another writer holds;
  4. 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 SlotBusy naming the last cursor tried;
  5. issues a release fence and writes the identifier, the length and the body;
  6. 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:

FieldSizeContents
magic4PMSW as little-endian bytes, 0x57534D50; tells Pulse Mesh traffic apart from anything else on the socket.
version11. Bumped on any layout-incompatible change.
seq8A per-run monotonic sequence number; feedback refers to it.
send_ns8The sender’s clock in nanoseconds, so a receiver can compute one-way latency without a side channel.
pulse128The Pulse’s bytes, verbatim.
n4The envelope length, 0 to MAX_ENVELOPE.
envelopenThe envelope’s canonical bytes, when present.
crc32c4A 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:

FrameBytesContents
ACKPMSA (0x41534D50) then a u64The receiver’s cursor: one past the highest sequence it has seen, whether or not every lower sequence has arrived.
NACKPMSN (0x4E534D50) then a u64One 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:

FieldSizeContents
magic4PMSF (0x46534D50).
window_start8The sequence number of the window’s first frame.
window_n2K, so a receiver detects a mismatched configuration.
parity_len4The length of the parity, the longest frame in the window.
parityparity_lenThe 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:

FieldSizeContents
magic4PMSH (0x48534D50).
version2The protocol version the sender speaks, at most 3 in this revision.
capabilities4A bit field: BACKPRESSURE 1, NACK 2, FEC 4, AF_XDP 8, DPDK 16, TLS_443 32.
registry_hash32The BLAKE3 hash of the sender’s context registry.
reserved6MUST 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

SurfaceStatusOn change
Chapter 2: the Pulse layout, alignment and instructionsNormativeDraft revision
Chapter 3: the canonical envelope bytes, the identifier, the variants, the nesting rulesNormativeDraft revision
Chapter 4: the ring protocolNormative for shared-memory substratesDraft revision
Chapter 5: the store layoutReference implementationLayout version bump, recorded in the decision log
Chapter 6: the UDP framing, feedback, parity and handshakeNormative for the reference transportDraft revision when the framing changes; other transports unaffected
Chapter 7: the port protocolNormative for the daemon’s clientsProtocol version bump
The vectors under pulse/pulse-spec/test-vectorsNormative, the executable contractDraft revision on any addition or change
The registration fields modules.toml (profiles repository) defines for a satellite (tier, registry status, targets, conformance surface)NormativeDraft revision when a meaning changes; entries added without a change of meaning do not
The reference crates, the tools, the examples and this bookReference and informativeRelease 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.

TierPathWhat lives here
0 — cornerstonepulse/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 — enginecore/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.

PathWhat 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 canonical mesh-lex registry.
  • 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 the mesh-lex registry, so it lives with the tools rather than in tier 0) — regenerates lex-root.txt into the path given, cargo run -p gen-lex-root -- pulse/pulse-spec/lex-root.txt, by computing the BLAKE3 Merkle root over every Boundary::Public entry in the mesh-lex registry. Today that registry is empty, so the root reduces to the domain-separated empty sentinel BLAKE3("mesh-lex/empty/v1").
  • tools/gen-test-vectors — regenerates test-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.

CratePurpose
core-backoffBackoff — 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-regionPulseRegion — 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-ringPulseRing — 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-storeEnvelopeStore — 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-transportThe 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-crc32cCRC32C 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-udpThe 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-registryTier 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/)

CratePathStatusDescription
gen-test-vectorspulse/pulse-spec/tools/gen-test-vectorsRegenerate the language-neutral conformance fixtures under test-vectors/.
mesh-capabilitypulse/mesh-capabilityPulse Mesh capability negotiation: per-node feature flags + version negotiation handshake.
mesh-contextpulse/mesh-contextpre-integrationPulse Mesh Context Registry / Book of Standards: 16-bit context_id + 16-bit subscope namespace partitioning of the Pulse scope field.
mesh-lexpulse/mesh-lexPulse Mesh public conformance substrate — minimum-viable lex registry + Merkle root over the registered catalog.
pulse-canonpulse/pulse-canonPulse Mesh canonical primitive: the 128-byte Pulse + PulseEnvelope codec + NodeId. Tier 0 (the shape).

The engine (tier 1, core/)

CratePathStatusDescription
core-backoffcore/core-backoffPulse Mesh backoff for OS-visible stalls: spin, then yield, then sleep in fixed steps. Tier 1 (the engine).
core-regioncore/core-regionPulse 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-ringcore/core-ringPulse Mesh SHM-backed SeqLock ring buffer (single-writer, N-reader). Tier 1 (the engine).
core-storecore/core-storePulse Mesh EnvelopeStore: SHM circular buffer for variable-size envelopes with content-addressed lookup. Tier 1 (the engine).
core-transportcore/core-transportPulse Mesh PulseSender / PulseReceiver trait family. Substrate-agnostic transport contract. Tier 1 (the engine).
mesh-stage-registrycore/mesh-stage-registryCompile-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-crc32ccore/pms-crc32cPulse Mesh CRC32C trailer: compute and verify the checksum a Pulse carries in its last four bytes. Module M10. Tier 1 (the engine).
pulse-wire-udpcore/pulse-wire-udpUDP 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/)

CratePathStatusDescription
example-01-hello-pulse-rustexamples/01-hello-pulse-rustExample 01 — encode + decode a PulseEnvelope, print its BLAKE3 content hash.
example-02-loopback-udp-rustexamples/02-loopback-udp-rustExample 02 — a sender and a receiver thread exchanging Pulses over UDP loopback in one process.
example-03-embed-ring-rustexamples/03-embed-ring-rustExample 03 — the shared-memory ring and the envelope store used directly, without the daemon.
gen-lex-roottools/gen-lex-rootRegenerate the published lex_root (pulse/pulse-spec/lex-root.txt) for the current public mesh-lex registry state, into the path given.
pulse-clitools/pulse-cliThe 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.

VariantMeaning
Openshm_open(3), fstat(2) or a header read failed; the field is the errno.
Truncateftruncate failed; the field is the errno.
NoSpaceLinux 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.
Mapmmap failed; the field is the errno.
InvalidNameThe name contains a NUL byte.
RegionTooSmallThe object is smaller than a region, [REGION_SIZE] bytes.
LayoutMismatchThe 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.

VariantMeaning
HugePageAllocMaking a memfd ([allocate_hugepage_memfd]) or duplicating a descriptor ([dup_fd]) failed; the string says what and why.
NumaPinPinning 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.
UnsupportedPageSizeCaller requested a page size the kernel doesn’t expose. Check /sys/kernel/mm/hugepages to see what’s available.
LibnumaMissingThe 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.
IoUnderlying I/O failure (typically while creating or sizing the SHM file).

Error in core/core-region

Defined in core/core-region/src/shm.rs.

VariantMeaning
Openshm_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).
Truncateftruncate(2) could not size the new object; the field is its errno (EFBIG, ENOSPC).
NoSpaceLinux 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.
Mapmmap(2) failed; the field is its errno.
InvalidNameThe name holds a NUL byte, which no C string can carry.
TooSmallThe 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.

VariantMeaning
Openshm_open(3), fstat(2) or a header read failed; the field is the errno.
Truncateftruncate failed; the field is the errno.
NoSpaceLinux 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.
Mapmmap failed; the field is the errno.
Unlinkshm_unlink failed; the field is the errno.
InvalidNameThe ring name contains a NUL byte, or is not a valid hub name.
InvalidCapacityZero 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.
BatchTooLargeA batch publish was given more pulses than the ring has slots; nothing was written. Publish at most capacity pulses per batch.
StalledA 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.
ReadersFullEvery line of the ring’s reader table is taken.
InvalidReadersA reader table of max_readers lines: it needs from 1 to [MAX_READERS]. Nothing was created.
NameTooLongA derived shared-memory name exceeds the platform limit.
RegionTooSmallThe object is smaller than the ring the caller asked to map, which would fault past its end.
LayoutMismatchThe 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.

VariantMeaning
Openshm_open(3), fstat(2) or a header read failed; the field is the errno.
Truncateftruncate failed; the field is the errno.
NoSpaceLinux 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.
Mapmmap failed; the field is the errno.
Unlinkshm_unlink failed; the field is the errno.
InvalidNameThe name contains a NUL byte.
InvalidCapacityZero 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.
RegionTooSmallThe 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.
LayoutMismatchThe header does not describe the layout this build uses.
SlotBusyEach 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.
InvalidIndexAn 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.
EnvelopeTooLargeThe envelope’s canonical encoding exceeds [MAX_ENTRY_SIZE] bytes.

TransportError in core/core-transport

Defined in core/core-transport/src/lib.rs.

VariantMeaning
IoUnderlying I/O failure (socket, file, kernel).
BufferTooSmallThe caller’s envelope buffer is smaller than the incoming envelope.
DecodeA frame failed the wire format’s validation (magic, version, length, CRC, or the backend’s own checks).
EnvelopeTooLargeThe envelope exceeds the transport’s max_envelope().
SendA 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).
RecvA 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.

VariantMeaning
TooShortBuffer is shorter than [FEC_HEADER_BYTES].
MagicMismatchFirst 4 bytes don’t match [FEC_MAGIC].
TruncatedLength-prefix says the parity payload is parity_len bytes but the buffer doesn’t actually carry that many.
InvalidWindowThe 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.

VariantMeaning
IoSocket I/O error. WouldBlock from a timeout is one of these.
DecodeThe datagram failed wire-format validation (magic, version, length or CRC).
BufferTooSmallThe envelope does not fit the buffer given to [Receiver::recv_into]; the frame is consumed.
InvalidPulseBytesA 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.

VariantMeaning
IoReading the first datagram failed, so nothing was read; a timeout (WouldBlock or TimedOut) means none arrived within the socket’s read timeout.
BatchTooLargemax_frames exceeded [crate::sender::MAX_BATCH_FRAMES].
InsufficientBuffersenvelopes.len() is smaller than max_frames.

SealError in core/pulse-wire-udp

Defined in core/pulse-wire-udp/src/sealed.rs.

VariantMeaning
IoThe socket refused a handshake message.
KeyFileThe key file could not be opened, created, read or written.
KeyFileExposedThe key file can be read or written by its group or by others, or is not a regular file; it is not loaded.
InvalidKeyText that should name a key is not 64 hex digits.
NotAllowedThe peer’s key is not on this node’s allowlist.
HandshakeTimeoutNo response to the handshake arrived within the timeout.
NoSessionThere is no session to the destination: [Sealing::connect] first, or the session was dropped, idle, or spent its counter.
CryptoThe Noise library or the system’s entropy source refused.

SenderError in core/pulse-wire-udp

Defined in core/pulse-wire-udp/src/sender.rs.

VariantMeaning
IoUnderlying socket I/O failure.
EncodeThe frame failed to encode: [WireError::EnvelopeTooLarge] for an envelope over [Sender::envelope_limit]. The sender does not fragment.
NoDestinationNo destination — caller must connect() or use [Sender::send_to].
StalledBackpressure 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.
SealSealing 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.

VariantMeaning
FrameErrorThe 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.
BatchTooLargeThe batch is longer than [MAX_BATCH_FRAMES]. Nothing was sent.
PartialSendsendmmsg(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.
Iosendmmsg(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.

VariantMeaning
BufferTooSmallThe buffer is shorter than a frame with no envelope, [HEADER_BYTES] + [TRAILER_BYTES].
InvalidEnvelopeLengthThe frame declares an envelope longer than [MAX_ENVELOPE] or than the buffer holds.
BadMagicMagic mismatch — datagram is not a pulse-wire-udp frame.
UnsupportedVersionVersion mismatch.
CrcMismatchCRC32C mismatch — wire-level bit-flip or mid-stream truncation; receivers count this toward loss metrics.
EnvelopeTooLargeThe 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.
InvalidFragmentA 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]).
EnvelopeCodecA decoded [Frame]’s envelope bytes are not a canonical [PulseEnvelope]. Returned only by [decode_with_envelope].
EnvelopeIdMismatchThe 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.

VariantMeaning
TooShortBuffer is shorter than [HELLO_BYTES].
MagicMismatchFirst 4 bytes don’t match [HELLO_MAGIC].
UnsupportedVersionRemote’s protocol version is newer than [MAX_VERSION].

ContextRegistryError in pulse/mesh-context

Defined in pulse/mesh-context/src/lib.rs.

VariantMeaning
IdConflictTwo different entries share a context_id.
TomlTOML parse failure on [ContextRegistry::from_toml].
JsonJSON failure in [ContextRegistry::from_json] or [ContextRegistry::to_json].

LexRegistryError in pulse/mesh-lex

Defined in pulse/mesh-lex/src/registry.rs.

VariantMeaning
DuplicateInsert 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.

VariantMeaning
BufferTooSmallBuffer shorter than the fixed-header + minimum tail.
TruncatedA length-prefixed field declared more bytes than buf contained.
InvalidTypeNameTagtype_name presence tag was neither 0 (None) nor 1 (Some).
InvalidTypeNametype_name bytes were present but not valid UTF-8.
NestedCountExceedsBufferA nested payload declares more children than the remaining bytes can hold, at [MIN_CANONICAL_ENVELOPE] bytes each.
NestingTooDeepNesting deeper than [MAX_NESTING_DEPTH], which the format itself does not limit.
InvalidPayloadVariantTagPayload variant tag was neither [PAYLOAD_VARIANT_RAW] nor [PAYLOAD_VARIANT_NESTED].
NestedDecodeA nested child failed to decode.
UnknownFlagsThe 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.

VariantMeaning
ReceiverSetupThe receiving socket could not be bound or configured.
SenderSender-side failure (bind, connect, or per-pulse send).
RecvReceiving or decoding a pulse failed.
SendThreadPanicThe sending thread panicked.

RoundtripError in tools/pulse-cli

Defined in tools/pulse-cli/src/ops/codec.rs.

VariantMeaning
DecodeFailedThe 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.

VariantMeaning
HexInput was not valid hex.
CodecDecoded bytes did not parse as a canonical envelope.

SendError in tools/pulse-cli

Defined in tools/pulse-cli/src/ops/udp.rs.

VariantMeaning
BindUnderlying socket bind failed.
SendUnderlying sender failed.

RecvError in tools/pulse-cli

Defined in tools/pulse-cli/src/ops/udp.rs.

VariantMeaning
BindThe socket could not be bound or configured.
RecvUnderlying 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 --open builds and opens it; every crate opens with an overview and every public item is documented, which the workspace’s missing_docs lint enforces. The Rust client for the daemon is documented in the daemon’s book.
  • Python bindings: the pulse_wire PyO3 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) over pulse-wire-udp; see C bindings.
  • bindings/python/ — a PyO3/maturin module named pulse_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 package pms, exposing Mesh/MeshEvent/attach.
  • clients/pms-node-thin/ (daemon repository) — a single-dependency (ws) Node package, class Mesh.

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

Part 04 · Concepts

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.

A Pulse travelling from a producer through the ring to a consumer, with the envelope store beside it
Figure A01 · end to end · producer, ring, store, transport, consumer

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.

TierWhereWhat lives thereColor in every figure
0pulse/, hereThe 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
1core/, hereThe 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
2mesh/, in the daemonThe 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
3the dev ring in the daemon; rings/ in the profilesDomain 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.

Two cores accessing adjacent slots with and without 64-byte alignment
Figure A04 · cache-line alignment · two cores, adjacent slots, with and without a 64-byte boundary

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 same content producing the same identifier in several languages
Figure A06 · content addressing · one preimage, one identifier, every language

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.

Recomputing an identifier and comparing it with the stored one
Figure A09 · verify · recompute and compare

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 SeqLock cycle: even, odd while writing, even again
Figure A10 · the SeqLock · even is stable, odd is a write in progress

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.

The reader protocol detecting a torn read
Figure A12 · the reader · two loads around the copy; a mismatch is a torn read

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 backpressure loop: the receiver's acknowledgement cursor throttling the sender
Figure A17 · backpressure · the acknowledgement cursor closes the loop

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.

A window of frames with one loss recovered from the parity frame
Figure A16 · forward error correction · one parity frame per window, one loss recovered

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.

ADRTitleStatusDate
ADR-0008Envelope store layout version 1Superseded in part by ADR-0011 (2026-09-24)2026-09-24
ADR-0011Envelope store layout version 2Accepted2026-09-24
ADR-0013One shared-memory mechanismAccepted2026-09-25
ADR-0014Pre-integration crates say soAccepted2026-09-25
ADR-0015Ring header layoutAccepted2026-09-24
ADR-0016Gated rings: one store per publish, one check per readAccepted2026-10-04
ADR-0017Partial crates say so, and the architecture check holds them to itAccepted2026-10-17
ADR-0018The security model: what is protected, by what, and what is notAccepted2026-10-17
ADR-0019A reader table for lossless fan-outAccepted2026-10-17
ADR-0020An index of recent cursors for the envelope storeAccepted2026-10-17

Changing the tree

Part 05 · Contributing

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.

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>.

KindJobShape
ExampleShow it workingOpens 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.
WorkflowDo one domain’s job end to endAn example’s shape, for one domain: robotics, enterprise, HFT or distributed compute. States which parts of the path are implemented and which are deferred.
TutorialTeach by doingStates 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 guideGet a task doneNames the task, the prerequisites, the steps in order with the expected result after each; links out for background and facts; no teaching.
ReferenceState what is trueComplete, precise, generated from the code where a table can be; no persuasion, no steps.
ExplanationSay whyOne 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.

  1. Is the page exactly one kind, and does its first line say which and for whom?
  2. Does every command in it run in the documentation gate, and does the output shown match?
  3. Is every fact traceable to code, a test, a fixture, or a bench command named on the page?
  4. Does an explanation page make one claim, show one figure, and say what would refute it?
  5. Are all terms the glossary’s terms, used in the glossary’s senses?
  6. Does the page carry no internal process language, no persona, and no name of something that does not exist?
  7. Do the version, the links and the summary pass the gate (just docs)?
  8. Is anything superseded marked rather than deleted, and is any decision it records in decisions/?
  9. Is it under the length limit, or split?
  10. Would a reader who knows nothing of how this tree was made understand every sentence?
  11. 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.