Example (Zeeguu): When the prompt that generates audio lesson scripts was improved, the ~900 stored audio_lesson_meaning rows produced under the previous prompt were neither regenerated eagerly nor deleted. Instead, each affected row received a deprecated_at timestamp, and the cache-lookup helper (AudioLessonMeaning.find()) was gated to skip deprecated rows. New daily lessons request a fresh row and trigger regeneration under the new prompt; existing daily lessons that already reference a deprecated row keep playing their old audio without breaking.
Forces: When a prompt or model improves, the obvious responses each have a serious drawback:
- Regenerate everything eagerly — expensive, floods generation queues if affected rows number in the thousands, and pays for content that may never be re-requested.
- Delete the stale rows — breaks any downstream object that references them by id (history, analytics, user-visible past sessions).
- Leave the stale rows in place and accept future reuse — silently propagates the old, known-suboptimal quality.
None of these are good defaults for production systems where LLM-generated artifacts are referenced from user-visible history and are also targets for reuse.
Solution: Mark stale rows as deprecated rather than mutating or removing them. Gate the cache-lookup / reuse path to skip deprecated rows, forcing fresh generation on next demand. Existing references to a deprecated row remain valid — the row keeps its content for historical playback — but no new consumer picks it up. Regeneration cost is paid lazily, amortized over normal access patterns, and only for content that is actually requested again.
Notes:
- This pattern is forward-only: it gates reuse, not playback. A user replaying an old lesson hears the old (lower-quality) version. That is usually preferable to a silent content swap mid-history.
- Works best when content has a clear “next access triggers regeneration” entry point. If consumers cache aggressively further downstream, the deprecation flag has to propagate to those layers too.
- Composes naturally with LLM Output Provenance (#7): provenance answers “which rows are stale?”, soft invalidation answers “what do I do with them once I know?”.
- Prerequisite — artifact identity must follow the row, not the upstream key. If the on-disk artifact (audio file, image, embedding) is named after the source identity (e.g.
meaning_id) rather than the row identity (e.g.audio_lesson_meaning.id), the regenerated row’s artifact overwrites the deprecated row’s artifact on the same path — defeating the historical-playback guarantee. Zeeguu encountered this concretely: meaning-lesson audio files were keyed bymeaning_id, so regeneration silently replaced the audio referenced by old daily lessons. A separate change re-keyed those files by row id to make Soft Invalidation safe. This small structural requirement may deserve being a pattern in its own right (working title: artifact identity = row identity).