Appearance
Request/reply
Request/reply is Matrix's primary call idiom. The caller publishes one envelope to the target's $inbox and awaits one response on a unique reply subject. From the caller's perspective it is synchronous (await); on the wire it is two pub/sub messages with a correlation ID linking them.
The shape
The envelope (projects/matrix-3/packages/core/src/engine/messaging/MxEnvelope.ts):
typescript
export interface MxEnvelope {
op: string; // operation name, e.g. 'registry.resolve'
payload: unknown; // the call arguments
iface?: string; // contracted interface name
correlationId?: string; // unique per request
replyTo?: string; // reply subject
lamport?: number; // logical clock
from?: string; // sender mount path
traceId?: string; spanId?: string; parentSpanId?: string; // tracing
}A request envelope sets op, payload, correlationId, and replyTo. A response envelope (delivered to replyTo) sets op: '$reply', the result in payload, and echoes the correlationId.
Wire flow
Subjects (root = COM.NIMBLETEC.RICHARD-SANTOMAURO):
1. caller subscribes → COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.f12d-7af0
2. caller publishes → COM.NIMBLETEC.RICHARD-SANTOMAURO.system.registry.$inbox
{ op: 'registry.resolve',
correlationId: 'f12d-7af0',
replyTo: '$replies.f12d-7af0',
payload: { logicalMount: 'chat.conversation' } }
3. registry actor handles → publishes response on
COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.f12d-7af0
{ op: '$reply', correlationId: 'f12d-7af0',
payload: { ok: true, providers: [...] } }
4. caller's subscription delivers the response, then unsubscribesThe transport accepts $replies.f12d-7af0 (semantic) and emits {root}.$reply.f12d-7af0 (wire). See projects/matrix-3/packages/core/src/transport/NatsTransport.ts lines 267–305.
NATS-protocol vs Matrix-form replies
NATS itself supports request/reply natively via the replyTo field on a message header. When the underlying transport is NATS and the connection exposes a request() method, RequestReply.execute may use the native NATS _INBOX.* reply subject for performance — a single subscription, no explicit reply pub.
When the transport does not expose request() (or for cross-root calls), the request/reply path falls back to the explicit $reply.{cid} form documented above. Both are interoperable: the actor handler does not know or care which form was used.
Note: the source comment in
RequestReply.tsline 318 explains: "Do not use natsClient.request() here: protocol reply inboxes (_INBOX.*) bypass our root-prefixed subjects and break ACL enforcement." The Matrix request path uses our$reply.{cid}form so every subject is root-prefixed and authorizable.
Default timeout
The default request timeout is 5000 ms (RequestReply.ts line 263: const timeoutMs = opts.timeoutMs ?? 5000;). Callers can override per request:
typescript
const result = await actor.invoke(targetMount, op, payload, { timeoutMs: 30_000 });When the timeout fires, the caller unsubscribes from the reply subject and the call rejects with a timeout error. See Timeouts for the full classification.
When to use request/reply
| Use request/reply | Use events instead |
|---|---|
| The caller needs the answer to proceed | The caller is broadcasting state changes |
| One specific actor must handle the call | Anyone interested may listen |
| The op modifies state (write) | The op is a notification (read-mostly) |
| The result is small enough to fit in one envelope | The output is a stream |
If the answer is multi-message (streaming, paginated, progress events), use a session on top of an initiating request/reply.
Idempotency
Per CLAUDE.md Rule 9: "Every write op accepts optional idempotencyKey." Callers that retry on timeout should pass the same idempotencyKey so the actor can deduplicate. Idempotency policy is per-actor; the framework does not auto-deduplicate.
What request/reply does NOT do
- It does not provide ordering guarantees across calls. Two parallel requests to the same actor may complete in any order.
- It does not retry. If the request times out or the actor returns an error, the caller decides whether to retry.
- It does not deliver the same response twice. The reply subject is unsubscribed after the first message arrives.
- It does not coordinate across multiple actors. Multi-actor workflows (sagas, two-phase commit) build on top of request/reply with explicit coordination ops.
See also
- Request envelope — exact field layout.
- Response envelope — success shape.
- Error envelope — error shape.
- Correlation IDs — generation and scope.
- Timeouts — defaults and classification.
- Subjects — wire-level naming.