refactor(agent): key the session-row heal on the classified cause
The heal predicate (errno 787 or a 'foreign key constraint' substring) and the classifier phrase were two separate definitions of the same failure. Move the SQLITE_CONSTRAINT_FOREIGNKEY code check into classify_persistence_error, document the bucket, and branch the heal on agent._last_persistence_error_cause == 'session_row_missing' so the two can't drift.
This commit is contained in:
@@ -5,7 +5,6 @@ import hashlib
|
||||
|
||||
import logging
|
||||
import re
|
||||
import sqlite3
|
||||
from contextlib import nullcontext
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -306,8 +305,7 @@ def _db_flush_failed(agent, e: Exception, batch_rows: List[Dict[str, Any]], adop
|
||||
from hermes_state import StateDbCorruptError, StateDbReplacedError, classify_persistence_error, divert_session_transcript_jsonl
|
||||
from hermes_state_errors import CompressionSessionClosedError
|
||||
agent._last_persistence_error_cause = classify_persistence_error(e)
|
||||
if getattr(e, "sqlite_errorcode", None) == getattr(sqlite3, "SQLITE_CONSTRAINT_FOREIGNKEY", 787) \
|
||||
or "foreign key constraint" in str(e).lower():
|
||||
if agent._last_persistence_error_cause == "session_row_missing":
|
||||
# The session row was removed under this live agent (`hermes sessions delete`, the Desktop/web
|
||||
# delete, bulk prune, a profile-repair move, an in-place store rebuild — none visible to the
|
||||
# cached agent, so the cached `_session_db_created` flag is stale and every later append hits
|
||||
|
||||
@@ -38,6 +38,7 @@ _TRANSIENT_SQLITE_MARKERS = (
|
||||
# on its %_config read but replaces the text with "vtable constructor failed: messages_fts",
|
||||
# so a phrase match read a busy store as a hard failure.
|
||||
_SQLITE_LOCK_CODES = (sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED)
|
||||
_SQLITE_CONSTRAINT_FOREIGNKEY = getattr(sqlite3, "SQLITE_CONSTRAINT_FOREIGNKEY", 787) # extended code
|
||||
|
||||
|
||||
def _sqlite_primary_code(exc_or_str) -> "int | None":
|
||||
@@ -273,7 +274,8 @@ def classify_persistence_error(exc_or_str) -> str:
|
||||
file damage (repair path, not disk space); "fts_index" = SQLite scoped the
|
||||
corruption to the FTS index (the transcript store is not damaged); "replaced" =
|
||||
main-file replacement; "deleted_wal" = a retired sidecar generation requiring
|
||||
capture inspection."""
|
||||
capture inspection; "session_row_missing" = the session row was deleted under a
|
||||
live agent (FK rejection; the flush recreates it)."""
|
||||
if exc_or_str is None:
|
||||
return "unknown"
|
||||
# Lease refusals contain neither "locked" nor "busy": match by type first,
|
||||
@@ -289,6 +291,8 @@ def classify_persistence_error(exc_or_str) -> str:
|
||||
return "fts_index"
|
||||
if _sqlite_primary_code(exc_or_str) in _SQLITE_LOCK_CODES:
|
||||
return "locked"
|
||||
if getattr(exc_or_str, "sqlite_errorcode", None) == _SQLITE_CONSTRAINT_FOREIGNKEY:
|
||||
return "session_row_missing"
|
||||
text = str(exc_or_str).lower()
|
||||
for markers, cause in _PERSISTENCE_CAUSE_BY_PHRASE:
|
||||
if any(marker in text for marker in markers):
|
||||
|
||||
Reference in New Issue
Block a user