The Migration Passed. Attestation Stopped.
The migration commit went in. The full test suite passed. The change did what it said: it moved the pair of rules that attest every agent event (hash it, sign it, check the signature) out of an old plugin and into a new, explicit stage of the event pipeline. Behaviour preserved. Tests green.
Then a fresh session’s record:
event_count=3, invocation_count=2, attestation_count=0
The session ledger for those two days agreed: zero of three sessions had any attestation counted.
For a platform whose first principle is what is not attested is not real, a session with no counted attestation is a session that, on the record, never happened in any form anyone could verify.
How both were true at once
The events were still being attested. The rules that produce and verify attestations had moved, and they worked in their new home. What stopped was the counting.
The counters had never lived with the attestation rules. They were emitted as a side effect of a different pass, inside the old plugin, which read attestation data off the event as it went by and incremented the session’s totals. The migration moved the producers into the new stage and then retired the old plugin, as planned. The counter emission lived in the old plugin. It was retired with it, before anything replaced it.
No test asserted that a real session, run end to end, ends with a non-zero attestation count. Tests checked that the rules attest. Nothing checked that the system witnessed the attestation. The work and the evidence of the work had separate owners, and only one of them moved.
It was the second time
In February, the same counters had read zero for a different reason. Two pipelines processed each event, one that annotated and one that attested, and the framework ran the annotating one first. The annotation step looked for attestation data that didn’t exist yet, so the counters were always zero. The fix then was to compute the counters after both pipelines had finished.
Two causes, the same symptom, the same field. That is the signature of a number that nobody owns: whoever happens to be nearby emits it, and whenever the neighbourhood changes, it goes quiet. Zero is a perfectly valid value for a counter, so a counter that stops counting looks exactly like a system with nothing to count.
Contained, and how we know
The damage was three local development sessions. Every cloud environment was still running the code from before the migration; the migration and its fix were scheduled to ship together. That containment came from release sequencing, not from any check, and it should be counted as sequencing rather than as a safeguard.
The live evidence did the finding. The suite did not, and could not: it had no test that ran a whole session and read the session record afterwards.
What changed
The counter reads the evidence, not the producer. The fix was about 20 lines. Two rules at the persistence stage read the typed attestation field on the event directly and emit the counts from it, regardless of which upstream step produced it. The attestation can come from anywhere; if it is on the event, it is counted. If it isn’t, nothing is.
The tempting alternative was to re-route the producer so it emitted the counter the old way, earlier in the pipeline. It would have worked. It would also have taught every future rule at that stage to chase the internal state of whatever happened to run before it, which is how the counters got orphaned twice.
There is now a test that fails if an attested event reaches storage without the counter moving, and one that fails if the counter moves for an event that carries no attestation. The second matters as much as the first: a counter that counts too generously is the same lie in the other direction.
And a gate for migrations like this one. Every rule that projects state onto the event must now be listed with its producer, its source, and its destination, and a CI check fails on any rule without that entry. A migration that moves a producer now has a written map of what depends on it, checked on every build, instead of an assumption.
The part worth keeping
A refactor can preserve behaviour and still remove evidence. Those are different properties, and test suites mostly check the first. In a system where attestation is the point, the evidence is behaviour, and the migration’s definition of done needs to include it: not do the rules still run? but does the record still show that they ran?
The line that stays
When a counter reads zero, the first question is not “is the system idle?” It is “who is responsible for this number, and did they move?”
A migration can keep everything working and stop everything being witnessed.