Mesh Contracts

Module — Stable. Optional at Core level. A Cloud Service that also provides the service-side feeds and trace propagation can claim to be meshed (§7.1).

Benzene Mesh is the application-level mesh: every service declares its full contract — the topics it serves, the events it publishes and the messages it sends to topics others define — so the fleet graph (catalog, who-calls-whom) is knowable statically, from registrations alone, before a single instance is deployed or a single message has flowed. Running code supplies a second, independent signal — health (§5) and, from the trace feed (§3), which declared edges are actually being exercised (§4.2) — but running code is never the source of the graph, only a witness to it. This document specifies everything that crosses a process boundary to make that work identically across language ports.

At Core level, mesh is optional, and so is each of its feeds; the normative degradation rule of §6 applies to every section here. A meshed service satisfies §7.1 — for it, §6 governs runtime degradation, not whether the feeds exist.

1. Self-description

A service describes itself once: the Contract Document, derived from its registrations — requests from its handlers, events and sends from its event and send declarations (contract-document §3.3) — and served at the reserved benzene:spec topic (contract-document §7). The mesh has no self-description of its own.

2. Registration

The body of a benzene:mesh:register message (§4) is the service's Contract Document — the same document benzene:spec serves, including the deployment-side fields (runtime, placement, profile, degraded, contract-document §1) the service knows. It carries registrationHash, which a collector trusts: it MAY recompute it to report a mismatch, but never rejects a registration for one, and compares heartbeats with the carried value (§5).

3. TraceEvent

One pipeline invocation as the mesh sees it — semantic (topic + Benzene status), not transport-shaped.

{
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "parentSpanId": "0af7651916cd43dd",
  "service": "orders",
  "instanceId": "orders-7f9c",
  "topic": "order:create",
  "topicVersion": "v2",
  "status": "validation-error",
  "exceptionType": "System.InvalidOperationException",
  "durationMs": 12.4,
  "startedAt": "2026-07-16T09:14:03.120Z",
  "correlationId": "abc-123"
}

4. Collector topics

A collector is an ordinary Benzene service serving these topics over any envelope-capable transport (transport-bindings.md):

Topic Body Success payload
benzene:mesh:register Contract Document (§2) {"accepted":1}
benzene:mesh:heartbeat Heartbeat (§5) {"accepted":1}
benzene:mesh:traces {"events":[TraceEvent…]} {"accepted":<count>}
benzene:mesh:issues IssueBatch (§4.1) {"accepted":<count>}

Sender behavior (normative for ports): trace export MUST be asynchronous, non-blocking, and lossy under backpressure — a full buffer drops events, a failed send drops the batch, and no mesh feed may ever fail, slow, or block the invocation it observed.

Collector behavior is pinned by conformance/mesh-collector-cases.json. The collector fixtures observe it through three query read models — benzene:mesh:query:fleet, benzene:mesh:query:service and benzene:mesh:query:topic — which are Provisional: their shapes are asserted only as far as the fixtures need, and full pinning follows 1.0. Other benzene:mesh:query:* topics a collector serves are its own.

A fleet view's trace summary — one recent flow — carries the flow's status: the status of its earliest-started failing event when the flow failed, otherwise of its earliest-started event. An event counts as failing exactly when it makes the summary failed. So a failed flow names the failure its caller saw, not the ok of whatever ran first, and a reader triaging flows does not have to open each one to learn how it ended. A collector that builds its flows from a trace store that does not hold the events' Benzene status (an external tracing backend) omits status; readers MUST tolerate its absence.

4.1 Issues (benzene:mesh:issues)

Optional on both sides (§6 applies): a collector that does not implement it is unaffected, and a service that does not emit it is reduced. It is not part of the meshed-service claim (§7.1, M1).

The pipeline-native failure feed: where traces record what happened, an issue records what is wrong — a deduplicated, classified failure signature emitted by the pipeline itself, which uniquely holds the wire status and the thrown exception at the moment of failure.

{
  "service": "orders",
  "issues": [
    {
      "fingerprint": "9f2c1a7e0d4b86513a9e2f70c4d1b8a2",
      "classification": "exception",
      "service": "orders",
      "topic": "order:create",
      "version": "v2",
      "transport": "sqs",
      "status": "service-unavailable",
      "exceptionType": "System.Net.Http.HttpRequestException",
      "count": 12,
      "firstSeen": "2026-07-25T09:14:03Z",
      "lastSeen": "2026-07-25T09:41:12Z",
      "exemplarTraceIds": ["4bf92f3577b34da6a3ce929d0e0e4736"],
      "resolutionHint": "deserialization"
    }
  ]
}

Pinned by conformance/mesh-issue-cases.json — required only for collectors claiming the issue feed (see conformance/README.md).

4.2 Declared vs. observed — liveness and drift

The graph (§4) is declared and does not need traffic to exist. Traces still matter — they are the only way to know whether a declared edge is actually being exercised, and the only way to notice an edge nobody declared. Both are collector-derived read models over the same two inputs (the registration's declared lists and TraceEvent parentage), never a change to either input:

Neither signal is itself a topology edge. A view MAY render "declared, unobserved" and "observed, undeclared" states distinctly from a confirmed (declared and observed) edge, but the graph (§4) itself contains only what's declared, unconditionally.

5. Heartbeat

The health-check aggregate response (wire-contracts.md §5) reused byte-for-byte, wrapped with identity:

{
  "service": "orders",
  "serviceVersion": "1.4.2",
  "instanceId": "orders-7f9c",
  "registrationHash": "sha256:…",
  "sentAt": "2026-07-16T09:14:03Z",
  "health": { "isHealthy": true, "healthChecks": { "db": { "status": "ok", "type": "postgres" } } }
}

serviceVersion is the instance's release, omitted when it declares none. A heartbeat whose registrationHash differs from the registered document's means the instance runs a contract the collector hasn't learned. The collector MUST surface the mismatch (the Go collector reports per-instance hashMatches) rather than silently keeping stale topics.

In a collector implementing Service Versions, this comparison is scoped to one service version: the heartbeat's hash is compared against the document registered for the same (service, serviceVersion) key, never against a different release of the same service. Two releases deployed side by side legitimately report different hashes, and a collector that compared across them would report drift for every side-by-side deployment — the opposite of what this rule is for. A heartbeat carrying a serviceVersion no document has registered is an unregistered instance, not a hash mismatch. A collector without the module compares against the service's one registration.

6. Degradation (normative)

Every mesh feed — registration, heartbeats, traces, issues — is independent and, at Core level, optional, on both sides (a meshed service provisions M1's feeds, §7.1, and this section governs their runtime degradation):

7. Conformance

7.1 A meshed service

A service that claims to be meshed — a full participant in the fleet view — MUST be a Cloud Service (R1–R5, R7) and additionally satisfy:

Runtime degradation (§6) does not withdraw the claim; deliberately unprovisioned feeds do.

7.2 Fixtures

Fixture files in conformance/ pin this document; their formats and the canonical mesh handlers are documented there.

A port that implements neither is unaffected at Core level — mesh is an optional module there, and the Core spec creates no obligation to implement it, only the obligation to implement it compatibly.

8. Conformance language note

Per the repository's one design rule, everything in §§1–6 is a Benzene concept — wire shapes and cross-process behavior. How a port derives its document (attribute scanning vs explicit registration), how a collector stores state, and what a view renders are idioms and stay out of this document.