IPC wire reference
Optional handshake, shared-memory layout and record rules for custom agent IPC clients.
Use this reference when implementing a client for the local agent's Linux shared-memory interface. For a working transaction sender, start with Use IPC and the standalone C example.
All multi-byte integers are little-endian. Access control words with aligned 32-bit or 64-bit atomic operations and the memory ordering below. Plain loads and stores are not sufficient for those words.
Session setup
- Connect a Unix
SOCK_STREAMsocket to the configuredlisten.ipcpath. - Send
5A 43 49 31 00: the ASCII magicZCI1followed by a zero key length. Send the hello within two seconds. The local agent uses its executable's license, so supply no API key. - Read the nine-byte reply with
recvmsg, collecting the shared-memory file descriptor fromSCM_RIGHTSancillary data. A stream read can be partial; collect all nine bytes and the descriptor before proceeding. - Validate the capacities and use
fstatto check the descriptor holds at least4096 + submission capacity + completion capacitybytes. Map that length from offset 0 withPROT_READ | PROT_WRITEandMAP_SHARED. - Validate the mapping header against the reply, including magic
ZCR1and version1. - Keep the socket open and switch it to nonblocking mode. It carries wake-up bytes; closing it ends the session.
| Reply offset | Size | Field |
|---|---|---|
| 0 | 1 | Reply code: 0 accepts the session |
| 1 | 4 | Submission capacity in bytes |
| 5 | 4 | Completion capacity in bytes |
Read capacities from the reply rather than hard-coding them. Each must be a power of two of at least 16 bytes. A refusal carries a nonzero code, zero capacities and no descriptor. Invalid handshake data can close the socket without a reply.
Each connection has its own mapping. Use one producer and one consumer per ring, or serialize access within your process. Separate sending threads can use separate sessions.
Shared-memory layout
| Offset | Size | Field | Writer |
|---|---|---|---|
| 0 | 4 | Magic ZCR1 | Agent, once |
| 4 | 4 | Version 1 | Agent, once |
| 8 | 4 | Submission capacity | Agent, once |
| 12 | 4 | Completion capacity | Agent, once |
| 64 | 8 | Submission tail | Client |
| 128 | 8 | Submission head | Agent |
| 192 | 8 | Completion tail | Agent |
| 256 | 8 | Completion head | Client |
| 320 | 4 | Agent parked flag | Agent sets; client clears |
| 384 | 4 | Client parked flag | Client sets; agent clears |
| 4096 | Submission capacity | Submission ring | Client |
| 4096 + submission capacity | Completion capacity | Completion ring | Agent |
The remaining header bytes are reserved. Leave them unchanged.
Ring rules
The client produces submissions and consumes completions. Each ring has one producer and one consumer.
Positions
Head and tail are monotonically increasing 64-bit byte counters, not offsets that wrap at the capacity. Both are multiples of 16, head <= tail, and tail - head <= capacity. The byte offset of a position is position % capacity.
Use acquire loads for the peer's counter and release stores to publish your own counter. Do not wrap a counter through zero.
Records
A record contains a 16-byte header, the payload, then zero padding to a multiple of 16 bytes. Its total size is (16 + payload_length + 15) & ~15.
| Header offset | Size | Field |
|---|---|---|
| 0 | 4 | Payload length, excluding header and padding |
| 4 | 1 | Kind |
| 5 | 1 | Flags |
| 6 | 2 | Reserved; write 0 |
| 8 | 8 | Request ID |
A record must not cross the ring boundary. If it would, publish a pad record in the remaining bytes before writing the real record at offset 0. The pad header has kind, flags, reserved and ID all zero; its length is remaining bytes - 16. Consumers skip the whole remainder, without interpreting its payload.
Writing a record
- Load your tail and acquire-load the consumer's head. Validate the positions.
- Calculate the aligned record size and any boundary pad. If their sum exceeds
capacity - (tail - head), wait for space without publishing the record. - Write any pad header, then the real header, payload and alignment padding.
- Release-store
tail + pad size + record sizeas the new tail. - Issue a sequentially consistent fence and wake the consumer if parked.
Reading a record
- Load your head and acquire-load the producer's tail. Validate alignment, ordering and the number of bytes in use. Equal positions mean the ring is empty.
- Validate the header and payload length before reading. A real record must fit within the ring boundary, the published bytes and the payload limit for its kind. A pad must have the required zero fields and cover exactly the published remainder of the ring.
- Copy the payload before advancing the head.
- Release-store
head + record size, orhead + remaining bytesfor a pad. The producer can overwrite those bytes after this store.
Submissions
| Kind | Payload | Flags |
|---|---|---|
1 | One signed transaction, bare or inside a payload envelope | 0x01 requests a completion. Also set 0x02 for preflight at processed commitment: flags 0x03 request both. |
3 | Bundle envelope containing one to four signed transactions | 0x01 requests a completion |
Payloads use the HTTP payload envelope, not a QUIC stream header. The submission payload limit is 16,403 bytes; the transaction limits still apply to each member.
Kind 1 requires a transaction payload and kind 3 requires a bundle payload. A mismatch produces malformed with problem: "expected_transaction" or "expected_bundle". Other kinds produce "invalid_record_kind"; unsupported flag bits produce "invalid_record_flags". These refusals affect the record, not the session.
The completion echoes the 64-bit request ID. Keep IDs distinct among outstanding requests in a session. Completions can arrive out of order.
Without 0x01, there is no completion, including on rejection, and no preflight. Use requests with replies while integrating.
Completions
| Field | Meaning |
|---|---|
| Kind | 3 for a bundle submission, otherwise 1 |
| Flags | Reply code; 0 means accepted |
| Request ID | The submission's ID |
Kind 1 payload | Empty on acceptance; rejection detail otherwise |
Kind 3 payload | 32-byte bundle ID, followed by rejection detail on refusal; a refused bundle has an all-zero ID |
Rejection detail is UTF-8 JSON of at most 65,535 bytes, using the error fields. The bundle ID is the raw 32-byte value returned as hexadecimal by sendBundle.
An accepted reply is not proof of landing. See agent acceptance and confirmation and retries.
Keep draining completions. If an error detail will not fit, the completion can contain only its code: an empty payload for kind 1, or 32 zero bytes for kind 3. If the completion ring has no room even for that, the completion is lost. Give every outstanding request a timeout.
Doorbells
A parked consumer waits on the Unix socket instead of spinning. The producer clears the consumer's parked flag and writes one byte to wake it. The byte's value is irrelevant.
To wait for completions:
- Release-store
1in the client parked flag at offset 384, then issue a sequentially consistent fence. - Acquire-load the completion tail again. If data arrived, clear the parked flag and read it without sleeping.
- Otherwise wait for socket readability, for example with
polland a request timeout. - Clear the flag with a release store, then drain wake-up bytes until the nonblocking read would block. A read of 0 means the session ended.
After publishing a submission:
- Issue a sequentially consistent fence after the release store to the submission tail.
- Acquire-load the agent parked flag at offset 320. If it is
1, exchange it with0using sequentially consistent ordering. - If the exchange returned
1, write a byte to the socket withMSG_NOSIGNAL. A failed or would-block wake-up write does not invalidate the published record.
The agent wakes the client the same way after publishing a completion. Implement doorbells even when busy polling is enabled, so the client also works with it disabled.
Session end and errors
- Closing the socket ends the session, but does not cancel submissions already published.
- A stop or update closes the agent's socket.
pollcan reportPOLLHUP, or a read returns 0. Completions already published remain readable in the mapping. - Invalid submission positions or record boundaries close the session without an error completion. Fix the producer before reconnecting.
Drain any published completions, unmap the old memory and connect again with a fresh handshake. Do not reuse the old mapping. Check transaction signatures before replaying requests whose outcomes are unknown.