Wire protocol
Protocol version: 2. v2 adds a per-frame HMAC envelope plus replay protection; v1 is not accepted on the wire. The canonical definitions live in z4j_core.transport.frames; see the WebSocket protocol reference for the full schema.
Transport
Section titled “Transport”- WebSocket, TLS in production (
wss://), plaintext for local dev (ws://). - One JSON object per WebSocket frame, UTF-8. There is no WS-layer batching; the
event_batchframe carries the application-level batch. - Authentication:
Authorization: Bearer <agent-token>on the WebSocket handshake. - HTTP longpoll fallback for networks that block WebSockets:
POST /api/v1/agent/eventsto upload signed frames andGET /api/v1/agent/commandsto poll for inbound commands. Samez4j_coreframe types, same HMAC envelope.
Frame envelope
Section titled “Frame envelope”Every frame carries the version, an id, and a type. Stateful frames also carry the HMAC envelope (nonce, seq, hmac); handshake frames (hello / hello_ack) are unsigned because the agent and brain are still negotiating which key to use.
{ "v": 2, "type": "event_batch", "id": "<agent-generated, 1..64 chars>", "ts": "2026-04-16T12:34:56.789Z", "nonce": "<urlsafe random>", "seq": 4281, "hmac": "<base64 HMAC over the canonical envelope>", "payload": { "events": [] }}Agent to brain
Section titled “Agent to brain”First frame the agent sends. Declares the agent's protocol version, framework, engines, schedulers, and host info. The brain validates compatibility before accepting the connection.
event_batch
Section titled “event_batch”The hot path. payload.events is capped at 5000 entries; the agent's batcher caps itself at 500. Signed in v2 so a stolen bearer token alone cannot forge events.
heartbeat
Section titled “heartbeat”Default cadence is 10 seconds; the brain returns its preferred heartbeat_interval_seconds in hello_ack and the agent honours that.
agent_status
Section titled “agent_status”Periodic self-report sent alongside the heartbeat: consecutive failure counts per error class, last success, session age, buffer depth and version metadata. The brain stores eligible frames in agent_status_history on a best-effort basis (rate limiting or a persistence failure may drop one).
command_result
Section titled “command_result”Reply to a brain-initiated command. Correlated by command_id.
command_ack
Section titled “command_ack”The agent sends this first-stage receipt immediately after accepting a
brain-initiated command and before executing it. The later command_result
reports the execution outcome.
registry_delta
Section titled “registry_delta”A task-definition delta for one engine (engine, added and updated definitions, removed names). The brain accepts the frame and discards it; nothing on the brain consumes registry deltas, and the agent runtime does not send them.
Brain to agent
Section titled “Brain to agent”hello_ack
Section titled “hello_ack”Carries agent_id, project_id, session_id, plus tuning parameters (heartbeat interval, max frame size).
event_batch_ack
Section titled “event_batch_ack”Round-trip ack so the agent knows which buffered batch it can drop. Includes the original frame id (acked_id) so the agent's in-flight map can match precisely.
command
Section titled “command”Brain-initiated work, correlated by command_id. The REST routes under /api/v1/projects/{slug}/commands/... issue retry_task, cancel_task, requeue_dead_letter, bulk_retry, purge_queue, restart_worker, pool_grow, pool_shrink, add_consumer, cancel_consumer and rate_limit; the dead-letter listing route (GET /api/v1/projects/{slug}/dead-letters) issues dlq.list and returns the agent's command_result as its HTTP response. The brain's own workers and schedule routes issue reconcile_task (the reconciliation sweep), submit_task (manual fire of a schedule owned by z4j-scheduler), schedule.fire, schedule.enable, schedule.disable, schedule.trigger_now, schedule.resync, schedule.external.activate and schedule.external.control. There is no schedule create or update on the wire: a schedule owned by z4j-scheduler is stored in the brain and reaches the agent as schedule.fire; a schedule owned by an external scheduler is inventoried by the agent's scheduler adapter, which receives only enable, disable, trigger_now and resync. target always carries type and id; a command the brain has resolved to one engine also carries engine in target (bulk_retry from its filter, dlq.list from the queue it reads), so a host running several adapters binds the right one. purge_queue carries no engine.
Carries code, message and fatal (default false). The brain's only emitter is scheduler_upgrade_required with fatal: true, sent when a schedule event needs a current scheduler adapter, and the brain does not close the socket after it. The agent closes on any fatal error frame, and treats the codes scheduler_upgrade_required, agent_incompatible and protocol_incompatible as terminal for its build (slow reconnect schedule until something is upgraded).
Sequence numbers and delivery
Section titled “Sequence numbers and delivery”- Agent to brain: the agent's
seqis monotonic per signed-frame stream. Replay protection rejects out-of-order or repeated nonces. - The brain dedupes accepted events by a deterministic event id: for a task event,
uuid5ofproject_id:engine:task_id:kind:occurred_at(at second precision); for any other event,uuid5ofproject_id:agent_event_id. A retried batch after a brain-side restart is therefore idempotent;seqis replay protection, not the dedupe key. - The
event_batch_ackcarriesreceived,accepted,rejectedso the agent can confirm exactly which rows landed. - Brain to agent:
command_idis a UUID. The agent replies with the samecommand_idincommand_result. Commands time out server-side: a sweeper (every 5 seconds by default) moves a command whosetimeout_athas passed to statustimeoutwith the errorcommand timed out before agent responded, and the REST side reads that from the command row.
Version negotiation
Section titled “Version negotiation”hello.payload.protocol_version must be "2". A mismatch closes the WebSocket with code 4426. The agent treats that close as terminal for its build: it keeps reconnecting, but on a slow schedule (120 seconds, backing off to 3600) instead of the normal one, so an upgraded deployment is picked up without restarting the host process.
Forward-compat: agents and brain ignore unknown fields. A verb an agent's adapter does not advertise is answered with command_result status: "failed" and the error adapter '<name>' does not support action '<verb>' (a verb the dispatcher has no branch for at all answers unrecognized action '<verb>'); the brain records that as a failed command rather than a protocol error.