fix: fail companion init loudly when persisted state cannot be loaded

A transient SQLite error during boot made companion_load_channels/
contacts/messages swallow the exception and return [], which is
indistinguishable from "no data". The Public-channel backfill then ran
over the empty store, so clients saw their channels wiped and later
saves could overwrite the persisted state.

- companion_load_{contacts,channels,messages} now return None on error
  vs [] for genuinely empty, and log the companion hash
- add companion_count_{channels,messages} helpers
- extract shared _restore_companion_state used by both the boot and
  hot-reload companion paths; each load is cross-checked against the
  table's row count for the hash, retried once after a short delay,
  and raises CompanionStateLoadError if it still cannot load, aborting
  companion init (and the Public backfill) instead of starting empty
- log restored row counts per companion; log when the channel store
  rejects a persisted channel (unchecked channels.set return)
- trim_companion_contacts_to_fit refuses to trim on a failed load
This commit is contained in:
agessaman
2026-07-07 07:43:27 -07:00
parent fd43d86ea8
commit e6d4b68d01
4 changed files with 428 additions and 124 deletions
+52 -12
View File
@@ -2392,8 +2392,12 @@ class SQLiteHandler:
logger.error(f"Failed to count companion contacts: {e}")
return 0
def companion_load_contacts(self, companion_hash: str) -> List[Dict]:
"""Load contacts for a companion from storage."""
def companion_load_contacts(self, companion_hash: str) -> Optional[List[Dict]]:
"""Load contacts for a companion from storage.
Returns [] when the companion has no persisted contacts, or None when
the load failed — callers must not treat a failed load as "no data".
"""
try:
with self._connect() as conn:
conn.row_factory = sqlite3.Row
@@ -2407,8 +2411,8 @@ class SQLiteHandler:
)
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
logger.error(f"Failed to load companion contacts: {e}")
return []
logger.error(f"Failed to load companion contacts for {companion_hash}: {e}")
return None
def companion_save_contacts(self, companion_hash: str, contacts: List[Dict]) -> bool:
"""Replace all contacts for a companion in storage using batch insert."""
@@ -2619,8 +2623,26 @@ class SQLiteHandler:
logger.error(f"Failed to save companion prefs: {e}")
return False
def companion_load_channels(self, companion_hash: str) -> List[Dict]:
"""Load channels for a companion from storage."""
def companion_count_channels(self, companion_hash: str) -> int:
"""Return the number of persisted channels for a companion."""
try:
with self._connect() as conn:
cursor = conn.execute(
"SELECT COUNT(*) FROM companion_channels WHERE companion_hash = ?",
(companion_hash,),
)
row = cursor.fetchone()
return int(row[0]) if row else 0
except Exception as e:
logger.error(f"Failed to count companion channels: {e}")
return 0
def companion_load_channels(self, companion_hash: str) -> Optional[List[Dict]]:
"""Load channels for a companion from storage.
Returns [] when the companion has no persisted channels, or None when
the load failed — callers must not treat a failed load as "no data".
"""
try:
with self._connect() as conn:
conn.row_factory = sqlite3.Row
@@ -2633,8 +2655,8 @@ class SQLiteHandler:
)
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
logger.error(f"Failed to load companion channels: {e}")
return []
logger.error(f"Failed to load companion channels for {companion_hash}: {e}")
return None
def companion_save_channels(self, companion_hash: str, channels: List[Dict]) -> bool:
"""Replace all channels for a companion in storage using batch insert."""
@@ -2670,8 +2692,26 @@ class SQLiteHandler:
logger.error(f"Failed to save companion channels: {e}")
return False
def companion_load_messages(self, companion_hash: str, limit: int = 100) -> List[Dict]:
"""Load queued messages for a companion (oldest first for queue order)."""
def companion_count_messages(self, companion_hash: str) -> int:
"""Return the number of persisted queued messages for a companion."""
try:
with self._connect() as conn:
cursor = conn.execute(
"SELECT COUNT(*) FROM companion_messages WHERE companion_hash = ?",
(companion_hash,),
)
row = cursor.fetchone()
return int(row[0]) if row else 0
except Exception as e:
logger.error(f"Failed to count companion messages: {e}")
return 0
def companion_load_messages(self, companion_hash: str, limit: int = 100) -> Optional[List[Dict]]:
"""Load queued messages for a companion (oldest first for queue order).
Returns [] when the companion has no persisted messages, or None when
the load failed — callers must not treat a failed load as "no data".
"""
try:
with self._connect() as conn:
conn.row_factory = sqlite3.Row
@@ -2689,8 +2729,8 @@ class SQLiteHandler:
msg["sender_prefix"] = bytes.fromhex(msg.get("sender_prefix") or "")
return rows
except Exception as e:
logger.error(f"Failed to load companion messages: {e}")
return []
logger.error(f"Failed to load companion messages for {companion_hash}: {e}")
return None
def companion_push_message(
self, companion_hash: str, msg: Dict, max_messages: Optional[int] = None