ZeroCopyZeroCopy
API reference

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

  1. Connect a Unix SOCK_STREAM socket to the configured listen.ipc path.
  2. Send 5A 43 49 31 00: the ASCII magic ZCI1 followed by a zero key length. Send the hello within two seconds. The local agent uses its executable's license, so supply no API key.
  3. Read the nine-byte reply with recvmsg, collecting the shared-memory file descriptor from SCM_RIGHTS ancillary data. A stream read can be partial; collect all nine bytes and the descriptor before proceeding.
  4. Validate the capacities and use fstat to check the descriptor holds at least 4096 + submission capacity + completion capacity bytes. Map that length from offset 0 with PROT_READ | PROT_WRITE and MAP_SHARED.
  5. Validate the mapping header against the reply, including magic ZCR1 and version 1.
  6. Keep the socket open and switch it to nonblocking mode. It carries wake-up bytes; closing it ends the session.
Reply offsetSizeField
01Reply code: 0 accepts the session
14Submission capacity in bytes
54Completion 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

OffsetSizeFieldWriter
04Magic ZCR1Agent, once
44Version 1Agent, once
84Submission capacityAgent, once
124Completion capacityAgent, once
648Submission tailClient
1288Submission headAgent
1928Completion tailAgent
2568Completion headClient
3204Agent parked flagAgent sets; client clears
3844Client parked flagClient sets; agent clears
4096Submission capacitySubmission ringClient
4096 + submission capacityCompletion capacityCompletion ringAgent

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 offsetSizeField
04Payload length, excluding header and padding
41Kind
51Flags
62Reserved; write 0
88Request 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

  1. Load your tail and acquire-load the consumer's head. Validate the positions.
  2. Calculate the aligned record size and any boundary pad. If their sum exceeds capacity - (tail - head), wait for space without publishing the record.
  3. Write any pad header, then the real header, payload and alignment padding.
  4. Release-store tail + pad size + record size as the new tail.
  5. Issue a sequentially consistent fence and wake the consumer if parked.

Reading a record

  1. 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.
  2. 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.
  3. Copy the payload before advancing the head.
  4. Release-store head + record size, or head + remaining bytes for a pad. The producer can overwrite those bytes after this store.

Submissions

KindPayloadFlags
1One signed transaction, bare or inside a payload envelope0x01 requests a completion. Also set 0x02 for preflight at processed commitment: flags 0x03 request both.
3Bundle envelope containing one to four signed transactions0x01 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

FieldMeaning
Kind3 for a bundle submission, otherwise 1
FlagsReply code; 0 means accepted
Request IDThe submission's ID
Kind 1 payloadEmpty on acceptance; rejection detail otherwise
Kind 3 payload32-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:

  1. Release-store 1 in the client parked flag at offset 384, then issue a sequentially consistent fence.
  2. Acquire-load the completion tail again. If data arrived, clear the parked flag and read it without sleeping.
  3. Otherwise wait for socket readability, for example with poll and a request timeout.
  4. 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:

  1. Issue a sequentially consistent fence after the release store to the submission tail.
  2. Acquire-load the agent parked flag at offset 320. If it is 1, exchange it with 0 using sequentially consistent ordering.
  3. If the exchange returned 1, write a byte to the socket with MSG_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. poll can report POLLHUP, 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.

On this page