Incident identity — who computes it, and what happens to the corpus when it changes
Systemorph/MeshWeaver.Plugins#1796, and the half #1795 deliberately left out.
An incident's fingerprint is its node id (Admin/_LogIncident/{fingerprint}). So the process
that computes it decides, for the whole corpus, what counts as "the same error happening again" —
and until this change that process was mw-log-watcher, an image with its own delivery lane.
What was measured
On the control instance, 2026-09-16/17, read-only:
| incident | reproduced from | still folding |
|---|---|---|
c399f09e8765b7ed (MeshWeaver#2387) |
SHA256("MeshWeaver.PluginCatalog.InstanceAutoRegistrationService\n0\n")[..8] |
2026-09-16T22:54:35Z |
b03482717d5ba39a (MeshWeaver#3659) |
same formula, …InstalledPackageRepairService |
2026-09-13 |
05f47831e3b7d6cd (MeshWeaver#2157) |
same formula, ProvisionPlan |
2026-09-13 |
That formula — category + eventId + exceptionType, no message — is the 2026-08-09 identity.
Four revisions later (#1002, #1170/#1171, #1787, #1795) the corpus was still addressed by it, which
has two consequences and they pull in opposite directions:
- Nothing splits. One incident per (category, exception type) means two unrelated defects raised from one class share a ticket forever. MeshWeaver#2387 was closed and reopened four times on occurrences of a different call site in the same log category.
- Nothing lands. Every correction to
StructuralLogIncidentIdentitywas invisible where it mattered, and each correction that DID reach an image re-addressed the corpus: the same fault starts computing a new id, opens a new incident and a new GitHub issue, while the old node sits open on a key nothing will ever compute again.
🚨 And a legacy node's identity cannot simply be recomputed offline. Admin/_LogIncident/9b70b639c4e77af3
— still folding bursts on the morning of 2026-09-17 — is reproduced by none of the plausible
payload shapes over its own stored category / exception / message / frame: the mint-time masking and
the fields the node retains have drifted apart. A sweep that recomputes the corpus is therefore
guessing, which is why the migration here is event-driven instead.
What changed
1. The portal resolves the identity, on ingest
LogIncidentIdentityResolution.Resolve(report, identity) runs in LogIncidentIngestService.Report.
The node is named after the PORTAL's answer; the reported fingerprint stays on the wire as the
reporter's key. A corrected identity now ships with the portal, which rolls continuously, and a
Code node implementing ILogIncidentIdentity finally overrides something — the extension point was
declared in the contract but nothing on the portal side consulted it.
Two shapes are never recomputed:
- A well-known id (
log-burst-header-only-{ns},log-ingest-refused-{ns}, …) — the pipeline's own findings are hand-written constants that BOTH sides synthesize, so recomputing one would fork it per reporter vintage. They are recognised by shape: a computed identity is always 16 lowercase hex characters, so the rule needs no new field on the wire and works for a watcher of any age. - A site fold (
Variants > 1) — the aggregator already collapsed a site that fanned out past its budget; splitting it here would open the fifty tickets the fold exists to prevent.
LogIncidentReport.EventId was added so the identity's site branch can be reproduced portal-side. A
watcher that predates the field sends nothing and it defaults to 0, which is what all but a handful
of log sites emit.
2. The legacy incident is CARRIED, not orphaned
When the reported id differs from the resolved one, the reported id names the legacy incident —
exactly, with no recomputation. On the first burst that lands on a successor,
LogIncidentIngestService.MigrateLegacy looks the legacy node up (an existence LISTING, never a
point read of a node that may be absent) and LogIncidentCorpusMigration does two writes:
- Fold the legacy history into the successor: occurrences, window, pods, samples, the shape ledger — and, only into an empty seat, the ticket (issue number and URL, repository, comment budget, draft, triage thread). One fault keeps ONE issue across the re-addressing, which is what stops a burst of duplicate issues when an identity revision lands.
- Supersede the legacy node:
Status = Superseded,SupersededBy = <successor>, nothing requested. It is kept, not deleted — it is the audit trail its GitHub issue was opened from — and the control plane never triages, files or comments from it again.
Idempotent, and it never mints a third node. FoldedFrom on the successor is the idempotency
key: a replayed report folds nothing a second time, so an inherited occurrence count cannot be
double-counted. The only nodes a migration touches are the legacy one and its successor. A legacy
node already superseded by a different successor contributes its relation but not its numbers.
At most one listing per incident, ever. The gate is the successor's stored
ReporterFingerprint: a node that has one has already been through here.
3. A bucket can be answered — the per-shape ledger
Re-addressing fixes the future; it does not make the incidents that ALREADY cover several defects
answerable, and a site fold is a bucket by design. LogIncident.Shapes is a bounded ledger
(12 rows, ShapesEvicted counts what fell out) keyed by what the current identity function computes
for each burst ALONE — so each row is the id that shape would have if it were split — with its own
FirstSeen / LastSeen / Occurrences.
That is the property that lets an issue be closed with evidence: an incident's own LastSeen
belongs to whichever shape fired last, so on a bucket it cannot say whether the defect the issue is
about has stopped. The shape's row can. The filer prints the table on the issue and on every
recurrence comment, with the sentence that reads it correctly.
What this does NOT do
- It does not migrate quiet legacy nodes. A legacy incident nothing reports any more is never
visited, because the move rides the burst that proves the fault is still live. That is deliberate:
a node nothing fires is, by definition, the closable case — its
LastSeenis the evidence — and minting an empty successor for it would add a node and a ticket for a fault that stopped. - It does not rename anything in place. Node ids are never rewritten;
issueNumber, the triage thread path and the bot's comment history stay attached to the node that earned them until they are deliberately carried. - It does not close the concurrent file-or-fold race (two issues for one fingerprint in the same second, MeshWeaver#4463/#4464). That is a third mode on the same thread and needs its own change.
Reading a corpus that is mid-migration
- A node with
SupersededByis history; read its successor. - A node with
FoldedFrominherited one or more legacy nodes; its counters include theirs. ReporterFingerprint != Fingerprintmeans the reporting watcher is running an older identity generation than the portal — which is normal and no longer costs anything.