ADR-004: Mercury Outbox
| Date | Author | Status |
|---|---|---|
| 2026-05-09 | Fabian Beyerlein | Accepted |
Context
All application messages that Mercury sends to the cloud need to be delivered in a durable manner. Currently, if Mercyry tries to send a message and it fails (either because network offline, session expired or handshake in progress, etc.) the message is lost forever. As these messages can be business critical operations like order confirmations or other transactional actions, message loss must be avoided.
Decision
Mercury's Tunnel module implements a outbox pattern. Instead of sending every
message directly to the gRPC stream, it is first written to the outbox
(Storage module based on etcd-io/bbolt). Then, a background process picks
up a batch of pending messages, processes them and marks them as in-flight.
Later, Nexus sends an ACK message back to Mercury. Based on the ACK status, Mercury either marks the message as processed, marks it as dead-lettered or marks it for retry.
Updated Message Envelope:
message AgentMessageEnvelope {
string message_id = 1; // globally unique, stable across retries
string correlation_id = 2;
uint64 sequence = 3;
google.protobuf.Timestamp timestamp = 4;
string message_type = 5;
bytes payload = 6;
map<string, string> metadata = 7;
uint32 attempt = 8;
oneof control {
TunnelAck ack = 100;
Pong pong = 101;
HeartbeatRequest heartbeat = 102;
}
}
message CloudMessageEnvelope {
string message_id = 1; // globally unique, stable across retries
string correlation_id = 2;
uint64 sequence = 3;
google.protobuf.Timestamp timestamp = 4;
string message_type = 5;
bytes payload = 6;
map<string, string> metadata = 7;
uint32 attempt = 8;
string targent_agent_id = 99;
oneof control {
TunnelAck ack = 100;
Ping ping = 101;
HeartbeatResponse heartbeat = 102;
Reconnect reconnect = 103;
}
}
Tunnel ACK
message TunnelAck {
string message_id = 1;
AckStatus status = 2;
string reason = 3;
}
enum AckStatus {
ASUnspecified = 0;
ASAccepted = 1;
ASRetryableError = 2;
ASPermanentlyRejected = 3;
}
Outbox Record
message OutboxRecord {
string message_id = 1;
OutboxRecordStatus status = 2;
uint32 attempts = 3;
bytes payload = 4;
google.protobuf.Timestamp enqueued_at = 5;
google.protobuf.Timestamp updated_at = 6;
optional google.protobuf.Timestamp next_retry_at = 7;
}
enum OutboxRecordStatus {
ORSUnspecified = 0;
ORSPending = 1;
ORSInFlight = 2;
ORSRetryable = 3;
ORSDeadLetter = 4;
ORSAcked = 5;
}