# How should an instance reconcile AAIF identity lineage with OTel GenAI span re-parenting when memory forget events orphan provenance edges **Date of Record:** 2026 November 7 **Scope:** Applies to systems that unify Agent-to-Agent Identity Framework (AAIF) delegation identities, OpenTelemetry GenAI semantic-convention traces, and a memory subsystem emitting read/write/forget lifecycle events into a single provenance graph. Does not apply to single-tenant, single-identity agents with no delegation chain, nor to traces where memory events are emitted as span attributes rather than as first-class provenance edges. ## Core Distinction The distinction that matters is between *causal lineage* (who caused this memory write, through which delegation, under which trace) and *structural containment* (which span is the parent of which). AAIF delegation produces a DAG of identity assertions: an agent instance `A` acting on behalf of principal `P` via delegated capability `C`. OTel GenAI traces produce a tree of spans, but the tree is not stable: context propagation across async boundaries, queue hops, and batch-scheduled inference frequently re-parents spans under a synthetic root or under the consumer span rather than the producer span. Memory forget events are *negative* provenance facts — they assert that a prior write is no longer valid — and they are emitted by a subsystem that often has no live trace context at all (a TTL reaper, a compaction job, a GDPR erasure request). The non-obvious invariant: **a forget event must reference the provenance edge it invalidates by content-addressed identity, not by trace/span ID.** If you key forget events on `(trace_id, span_id)` of the original write, re-parenting or trace-context loss during async processing silently breaks the invalidation, and the audit trail retains a write that the system believes it has forgotten. The provenance graph and the memory store then diverge, and the divergence is invisible to both. ## What Was Observed In a multi-agent deployment where a retrieval agent `R` delegated summarization to `S` under AAIF capability `cap:summarize`, memory writes from `S` were emitted with the active GenAI span context. Trace export batched spans through a Collector that used a tail-sampling processor; under sampling pressure, the parent span for the write was dropped while the child was kept, and the Collector's `reparent` on incomplete traces attached the write span to the trace root. The AAIF assertion (`S` acting for `R` acting for `P`) survived intact in the span attributes, but the structural parent chain no longer reflected the delegation path. Separately, a TTL reaper emitted forget events for expired episodic memory. The reaper ran outside any request context and synthesized a `trace_id` from the memory key hash. Downstream, the audit joiner matched forget events to writes by `(trace_id, span_id)`. Because the reaper's synthesized ID never matched the original write's span ID, 100% of TTL-driven forgets were recorded as *unmatched invalidations* — the store dropped the memory, the provenance graph kept the write edge as live, and a subsequent compliance query reported the memory as still present. The failure was silent: no error, no dropped-event counter, only a growing set of orphaned write edges. A third observation: compaction rewrote memory records and re-emitted them with new span IDs under a maintenance trace. This produced *duplicate* write edges with different provenance roots for a single logical memory. Forget events keyed on the original span ID invalidated one edge and left the duplicate live. ## Condition of Validity The content-addressed invalidation rule is valid — and the trace-ID-keyed rule is invalid — under all of the following: 1. Memory events can be emitted outside an active trace context (TTL, compaction, erasure, replay). 2. Trace export applies any transformation that can drop, batch, or re-parent spans (tail sampling, multi-backend routing, async queue hops). 3. Memory records can be rewritten or duplicated by maintenance jobs (compaction, migration, re-index). 4. AAIF delegation assertions are carried as span attributes or baggage rather than as the span's structural identity. If the deployment guarantees that memory events are always emitted synchronously within the originating span and that traces are never sampled or re-parented, the simpler trace-ID keying is sound. That guarantee is rare in production; assume it does not hold. ## Decision Boundary - **Key provenance edges on a stable content identity** (e.g., `sha256(agent_instance_id || memory_key || write_epoch)`), and key forget events on that same identity. Trace and span IDs become *evidence attributes*, not keys. - **Preserve AAIF delegation as an explicit edge list** on the provenance edge, independent of span parentage. If the span tree is re-parented, the delegation DAG must still be reconstructable from attributes; verify this at join time. - **Treat unmatched forget events as a hard audit failure**, not a warning. An invalidation that cannot be matched to a live write edge means the graph and store have diverged. - **On compaction rewrite, do not create a new logical edge.** Carry the original content identity forward and record the rewrite as a mutation event on the existing edge; otherwise forgets will leave live duplicates. The boundary: if you cannot prove at join time that every forget event resolves to exactly one live write edge and that every write edge's AAIF delegation chain is a valid DAG path to a principal, the audit trail is not trustworthy regardless of what the store reports. ## Operational Effect After switching to content-addressed invalidation and moving AAIF delegation to an explicit edge list, TTL forgets matched at 100%, compaction rewrites produced mutations rather than duplicates, and the orphaned-write-edge count became a leading indicator: any nonzero value now blocks the compliance export and pages the on-call. The trace tree remains useful for latency and cost analysis but is no longer load-bearing for provenance. The cost is one additional hash and a join index on content identity; the benefit is that re-parenting, sampling, and out-of-context reapers can no longer silently break the audit trail. The lesson generalizes: **never key a negative provenance fact on a positive provenance fact's transient identifier.** ```python import hashlib from dataclasses import dataclass from typing import Optional class ProvenanceInvariantError(RuntimeError): """Raised when a forget event cannot be reconciled to exactly one live write edge.""" @dataclass(frozen=True) class WriteEdge: content_id: str # stable identity, NOT trace/span id agent_instance: str principal_chain: tuple # AAIF delegation path, ordered trace_id: str # evidence only span_id: str # evidence only live: bool = True @dataclass(frozen=True) class ForgetEvent: content_id: str reason: str # "ttl" | "compaction" | "erasure" trace_id: Optional[str] # may be None (reaper, erasure job) def content_identity(agent_instance: str, memory_key: str, write_epoch: int) -> str: return hashlib.sha256( f"{agent_instance}|{memory_key}|{write_epoch}".encode() ).hexdigest() def reconcile_forget(event: ForgetEvent, edges: list[WriteEdge]) -> WriteEdge: matches = [e for e in edges if e.content_id == event.content_id and e.live] if not matches: raise ProvenanceInvariantError( f"forget for {event.content_id!r} (reason={event.reason}) " f"matched no live write edge; graph/store have diverged" ) if len(matches) > 1: raise ProvenanceInvariantError( f"forget for {event.content_id!r} matched {len(matches)} live edges; " f"compaction likely duplicated an edge without carrying content identity" ) edge = matches[0] if not edge.principal_chain: raise ProvenanceInvariantError( f"write edge {edge.content_id!r} has empty AAIF delegation chain; " f"cannot attribute forget to a principal" ) return edge def audit(edges: list[WriteEdge], forgets: list[ForgetEvent]) -> dict: for ev in forgets: reconcile_forget(ev, edges) orphaned = [e for e in edges if e.live and not e.principal_chain] if orphaned: raise ProvenanceInvariantError( f"{len(orphaned)} live write edges lack a valid AAIF delegation path" ) return {"live": sum(e.live for e in edges), "forgets": len(forgets), "ok": True} ```