Checklist
Describe the issue
After upgrading to v1.6.0 (which migrates storage from events.json to events.db), all snapshot files accumulated before the upgrade become permanent orphans that are never deleted — even with retention_time correctly configured.
Observed: 7,136 snapshot files dating back months, totalling 2.5 GB, with only 44 recent files actually referenced in the database. No cleanup had occurred despite retention_time: 7 being set.
Root cause
The bug is a race condition introduced in Timeline.__init__ in v1.6.0. All four async tasks are fired concurrently, but _migrating is set to True before any of them run:
# timeline.py — Timeline.__init__
self._migrating = True # set immediately
self.hass.async_create_task(self._initialize_db())
self.hass.async_create_task(self._migrate()) # sets _migrating=False when done
self.hass.async_create_task(self.load_events())
self.hass.async_create_task(self._cleanup()) # ← sees _migrating=True → returns immediately
_cleanup() has this guard (line 938):
if getattr(self, "_migrating", False):
return
Because all tasks run concurrently, _cleanup() always sees _migrating = True when a migration is in progress. Once _migrate() finishes and sets _migrating = False, there is no subsequent cleanup scheduled — so every snapshot file that pre-dates the migration becomes a permanent orphan.
The same guard also blocks the await timeline._cleanup() call in async_setup_entry (__init__.py line 137) if the background migration task hasn't completed yet.
Impact
Any user who upgraded from a version using events.json to v1.6.0+ will have unbounded snapshot accumulation. The retention system appears to be working (DB records are purged via _purge_expired_events) but the physical files are never swept because _cleanup() never runs successfully post-migration.
Note: this is a regression from the fix in v1.5.2 for issue #445 — the general retention case works correctly on a fresh install, but the one-time migration path bypasses cleanup entirely.
Suggested fix
Call _cleanup() at the end of _migrate() after setting _migrating = False, rather than racing it as a concurrent task:
async def _migrate(self):
try:
# ... migration logic ...
finally:
self._migrating = False
await self._cleanup() # run cleanup now that migration is complete
This ensures that any files orphaned by the migration (i.e., files that existed before events.db was created and are not referenced by any DB record) are swept immediately.
Workaround
Until a fix is released, users affected by this can run a one-time cleanup manually:
import sqlite3, os
DB_PATH = "/homeassistant/llmvision/events.db"
SNAPSHOTS_DIR = "/media/llmvision/snapshots"
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
cur.execute("SELECT key_frame FROM events WHERE key_frame IS NOT NULL")
protected = {os.path.basename(row[0]).lower() for row in cur.fetchall()}
con.close()
removed = 0
for f in os.listdir(SNAPSHOTS_DIR):
if f.lower() not in protected:
os.remove(os.path.join(SNAPSHOTS_DIR, f))
removed += 1
print(f"Removed {removed} orphaned snapshots")
Version
- LLM Vision: 1.6.0
- Home Assistant: 2025.10.4
Checklist
Describe the issue
After upgrading to v1.6.0 (which migrates storage from
events.jsontoevents.db), all snapshot files accumulated before the upgrade become permanent orphans that are never deleted — even withretention_timecorrectly configured.Observed: 7,136 snapshot files dating back months, totalling 2.5 GB, with only 44 recent files actually referenced in the database. No cleanup had occurred despite
retention_time: 7being set.Root cause
The bug is a race condition introduced in
Timeline.__init__in v1.6.0. All four async tasks are fired concurrently, but_migratingis set toTruebefore any of them run:_cleanup()has this guard (line 938):Because all tasks run concurrently,
_cleanup()always sees_migrating = Truewhen a migration is in progress. Once_migrate()finishes and sets_migrating = False, there is no subsequent cleanup scheduled — so every snapshot file that pre-dates the migration becomes a permanent orphan.The same guard also blocks the
await timeline._cleanup()call inasync_setup_entry(__init__.pyline 137) if the background migration task hasn't completed yet.Impact
Any user who upgraded from a version using
events.jsonto v1.6.0+ will have unbounded snapshot accumulation. The retention system appears to be working (DB records are purged via_purge_expired_events) but the physical files are never swept because_cleanup()never runs successfully post-migration.Note: this is a regression from the fix in v1.5.2 for issue #445 — the general retention case works correctly on a fresh install, but the one-time migration path bypasses cleanup entirely.
Suggested fix
Call
_cleanup()at the end of_migrate()after setting_migrating = False, rather than racing it as a concurrent task:This ensures that any files orphaned by the migration (i.e., files that existed before
events.dbwas created and are not referenced by any DB record) are swept immediately.Workaround
Until a fix is released, users affected by this can run a one-time cleanup manually:
Version