Skip to content

15. The write-up

Built: crates/mira/src/rca.rs, and the ninth entry in mcp.rs's tool list. render_rca takes an investigation — the one section 14 made possible by putting the kill on the same timeline as the telemetry — and returns it as markdown, having re-run every citation in it first. Zero new crates: the query executor, the alerting rule parser and the ingest pipeline were all already there.

15.1 A template would not have been worth a tool

render_rca is the only MCP tool that is not a question the UI also asks. It takes the findings as fields — summary, impact, timeline, root cause, evidence, what was ruled out, remediation, verification, prevention — and returns markdown. The prompt could have carried a template. Two things make this a tool instead.

Every citation is re-run before the document renders

An evidence item gives a trace_id or a query, and Mira executes it with limit: 0 — the same free count section 13.2 uses — and puts the row count beside the claim. A citation matching nothing fails the whole call, naming the claim; nothing is rendered and nothing is stored.

This is the part that is not a formatting convenience. An RCA whose evidence has aged out of retention reads exactly like one whose evidence was invented, and a model reasoning for twenty minutes has usually accumulated one claim it can no longer support. An item with no citation is refused at parse — judgement belongs in root_cause, which is not a field anything can check. A citation with no window of its own inherits the incident's, which is the difference between a working tool and one where every write-up of this morning fails its own verification.

The proposed rule goes through the alerting loader

prevention is an alerts.kyaml rule, parsed by the same alert::rule that reads the file at boot, so the fence in the document is a rule that will start rather than one that looks like it would. That is the fourth reading of principle 2 — self-tuning — reduced to its only honest mechanism: the incident emits the rule that would have caught it, and a human decides whether to add it.

Timestamps in the document are UTC. tui::stamp is local, which is right on a terminal somebody is sitting at and wrong in a ticket read from another timezone.

15.2 What comes back

The call, abbreviated:

title: "Checkout 502s: the connection pool never grew with the image"
from: 1789812000000000000
to:   1789813860000000000
root_cause: >-
  1.4.0 raised the pool ceiling to 200 without raising the memory limit, so
  the pod crossed its limit under normal load and the kubelet killed it.
evidence: [
  { claim: "checkout logged pool-exhaustion errors throughout the window",
    query: { where: [ { attr: service.name, eq: checkout },
                      { field: severity_number, gte: 17 } ] } },
  { claim: "the kubelet killed the container twice",
    query: { signal: logs,
             where: [ { attr: k8s.container.reason, eq: OOMKilled } ] } },
  { claim: "a failing request, end to end",
    trace_id: 4bf92f3577b34da6a3ce929d0e0e4736 },
]
prevention: {
  name:  checkout-oomkilled,
  query: { signal: logs,
           where: [ { attr: k8s.container.reason, eq: OOMKilled },
                    { attr: k8s.namespace.name, eq: shop } ] },
  over:  5m, when: "count >= 1", severity: critical,
}
emit: true

The second citation is the one section 14 exists for: k8s.container.reason is a field the kubelet holds and OTLP has never carried. The parts of the reply that are not the agent's own prose:

*2026-09-19T10:00:00Z → 2026-09-19T10:31:00Z (31m00s). Written by an agent
against Mira; every citation below was re-run at render time and returned the
record count beside it.*

## Evidence

- checkout logged pool-exhaustion errors throughout the window — logs where
  `attr:service.name=checkout field:severity_number>=17`, 4102 records
- the kubelet killed the container twice — logs where
  `attr:k8s.container.reason=OOMKilled`, 2 records
- a failing request, end to end — trace `4bf92f3577b34da6a3ce929d0e0e4736`,
  37 records

## Remediation

- Roll back to 1.3.9.
- Raise the memory limit to 1Gi before rolling 1.4.0 forward.

> Mira did not apply any of this and cannot: it has no verb that changes a
> cluster.

That last line is in the document, not only in this repository's docs (section 14.6). The reader of an RCA is the one who needs to know none of it has happened yet.

Sections with nothing in them are absent rather than empty: a bare ## Prevention invites the reader to assume there was none to propose.

15.3 emit, and why there is no incident store

With emit, the rendered document is also ingested — as a log record, through the pipeline an OTLP export goes through, with event_name: rca and the title and window as attributes.

A log record and not a new kind of object, and that is why it is affordable. There is no incidents table, no index to maintain, nothing extra to back up and no separate expiry: the write-up is searchable with query_records, framed by correlate, and aged out by the retention that ages the telemetry it describes. Principle 4 says the block directory is the manifest, and an incident store would have been a second one.

service.name on the record is mira, not the service the RCA is about. Naming the subject would file the document under that service's entity key and put a page of markdown in the middle of its logs — section 14.4 again, from the other direction.

The engine's ingest handle is the one place a read surface can write, and the alert evaluator's copy of Api is deliberately built without it. That task never returns, so a handle inside it is one the logs flusher never sees dropped, and every shutdown on an alerting node would sit out the full drain grace.