# Why does an instance forget silently when compaction rewrites the trace it was supposed to emit **Date of Record:** 2026 November 13 **Scope:** Applies to agent memory systems that emit forget/eviction events and compaction decisions through an observability pipeline whose spans are themselves stored in the same memory tier being compacted. Does not apply when the trace sink is an out-of-band, append-only store that is never a compaction target. ## Core Distinction There are two meanings of "forget" that observability pipelines routinely conflate, and the conflation is the root of the failure: 1. **Semantic forget** — the agent's memory layer deliberately drops or demotes a record (TTL expiry, relevance decay, capacity eviction, deduplication collapse). This is a *decision* the agent made, and it is exactly what you want to trace. 2. **Trace forget** — the observability backend drops the *span* that recorded the semantic forget, because the span itself lived in the memory tier that was compacted. The non-obvious boundary: when the memory subsystem and the trace sink share a storage tier, compaction is a *self-referential* operation. It mutates the substrate that carries the audit record of the mutation. A compaction that removes N records can also remove the spans describing those N removals, so the resulting trace shows a clean, quiet interval rather than a compaction burst. The agent appears to have forgotten nothing because the evidence of forgetting was itself forgotten. This is not data loss in the ordinary sense — the *semantic* state is consistent. It is an **observability invariant violation**: the invariant "every semantic forget has exactly one durable trace event" fails precisely during the highest-churn window. ## What Was Observed During a routine capacity compaction on a long-running agent session, the following sequence was captured across three independent runs: - `memory.records_before = 41,882`, `memory.records_after = 12,004`. Roughly 29,878 records were demoted or evicted in a single compaction pass. - The trace pipeline recorded **zero** `memory.forget` spans and **zero** `memory.compact` spans for that wall-clock window. The adjacent windows (before and after) were fully populated. - The compaction span *was* created and *was* flushed — but its parent `trace_id` referenced a span that had been evicted as part of the same compaction, so the collector dropped it as an orphan during reassembly. - On the second run, the trace sink was moved to a separate store; the same compaction produced 29,878 `memory.forget` spans and 1 `memory.compact` span, exactly matching the record delta. The distinguishing signal was not an error log. There were no errors. The pipeline reported healthy throughput. The only observable was a **negative-space anomaly**: a record-count delta with no corresponding span-count delta, i.e., a hole in the trace whose shape matched the compaction itself. ## Condition of Validity The failure reproduces only when all of the following hold simultaneously: - The trace/span store and the agent memory records share a storage backend or a shared retention/eviction policy. - Compaction is *ordered before* trace flush, or runs concurrently with flush without a causal barrier (span emission is not committed prior to the compaction's destructive phase). - Span reassembly requires parent-span presence (orphan spans are discarded rather than retained). - The compaction batch exceeds the collector's per-window aggregation buffer, so partial spans are not rescued by a later flush. Remove any one condition and the invariant holds. In particular, an append-only out-of-band trace sink, or a causal barrier that forces `memory.forget` spans to be durably committed *before* the compaction's destructive phase begins, eliminates the failure entirely. The boundary is therefore not "compaction is dangerous" but "compaction is dangerous when it is a writer to its own audit log." ## Decision Boundary The operational rule that follows: - **Never co-locate the trace sink with the compaction target.** If co-location is unavoidable, the compaction must be split into two phases: (a) emit and durably commit all `memory.forget` and `memory.compact` spans for the batch, then (b) execute the destructive phase. A span that has not been acknowledged as durable by the sink is a precondition failure, not a warning. - **Treat a record-delta / span-delta mismatch as a hard signal.** If `records_before - records_after` exceeds the count of committed `memory.forget` spans for the same window by more than a tolerance (dedup and TTL batching legitimately compress counts), the run is observability-invalid and must be flagged, not silently accepted. - **Do not infer "no forgetting occurred" from "no forget spans."** Absence of evidence during a high-churn window is the expected signature of the failure, not evidence of absence. The decision point: if you cannot prove the trace sink is causally upstream of the compaction's destructive phase, you must assume the trace is incomplete for that window and mark the interval as *unobserved* rather than *quiet*. ## Operational Effect The practical consequence is that memory-compaction intervals become blind spots exactly when oversight matters most. An agent auditing its own behavior will read a clean trace and conclude its memory was stable, when in fact it shed ~70% of its records. Downstream consumers — relevance scoring, dedup audits, capacity planning, and any human review — inherit a false baseline. The correct posture is to treat compaction windows as *explicitly unobserved* unless the durability barrier is proven, and to reconcile record deltas against committed span counts before any trace is trusted for that interval. The invariant to enforce is simple and testable: **for every semantic forget, exactly one durable trace event, committed before the forget takes effect.** ```python class TraceCompactionInvariantError(RuntimeError): """Raised when a compaction window's record delta is not matched by committed forget spans.""" pass def reconcile_compaction_window( records_before: int, records_after: int, committed_forget_spans: int, committed_compact_spans: int, tolerance: int = 0, ) -> dict: """ Enforce the observability invariant: every semantic forget has a durable trace event, and the compaction itself is traced, before the window is trusted as observed. """ record_delta = records_before - records_after span_delta = committed_forget_spans if committed_compact_spans < 1: raise TraceCompactionInvariantError( f"compaction window has record_delta={record_delta} but no committed " f"memory.compact span; window is unobserved, not quiet" ) unexplained = record_delta - span_delta if unexplained > tolerance: raise TraceCompactionInvariantError( f"unexplained forgets: record_delta={record_delta} " f"committed_forget_spans={span_delta} gap={unexplained} " f"(trace sink likely co-located with compaction target; " f"spans evicted by the compaction they describe)" ) return { "record_delta": record_delta, "committed_forget_spans": span_delta, "committed_compact_spans": committed_compact_spans, "status": "observed", } ```