refactor(hermes_cli): tighten session_recovery verification and salvage bodies

This commit is contained in:
Teknium
2026-09-02 22:39:51 -07:00
parent 9dcff9d5d0
commit 0bcade22e0
+24 -52
View File
@@ -582,9 +582,7 @@ class _RowidRangeSalvage:
last_committed_rowid = int(fetched[-1][0])
if self.progress_cb is not None:
self.progress_cb({
"table": self.table,
"copied_rows": result["copied_rows"],
"source_rows": self.source_rows,
"table": self.table, "copied_rows": result["copied_rows"], "source_rows": self.source_rows,
"skipped_ranges": len(result["skipped_rowid_ranges"]),
})
except sqlite3.DatabaseError as exc:
@@ -620,11 +618,9 @@ def _copy_table_salvage(
result["status"] = "complete"
return result
if bounds.get("low") is None or bounds.get("high") is None:
result["status"] = "failed"
details = "; ".join(bounds.get("errors") or [])
result["error"] = "could not determine a rowid range for salvage"
if details:
result["error"] += f": {details}"
result["status"] = "failed"
result["error"] = "could not determine a rowid range for salvage" + (f": {details}" if details else "")
return result
salvage = _RowidRangeSalvage(
@@ -639,12 +635,10 @@ def _copy_table_salvage(
if skipped_ranges:
result["status"] = "partial" if result["copied_rows"] else "failed"
result["error"] = f"{len(skipped_ranges)} rowid range(s) skipped"
elif (source_rows is not None and result["copied_rows"] + result["excluded_rows"] != source_rows):
elif source_rows is not None and result["copied_rows"] + result["excluded_rows"] != source_rows:
result["status"] = "partial"
result["error"] = (
f"copied {result['copied_rows']} and excluded "
f"{result['excluded_rows']} of {source_rows} source rows"
)
copied, excluded = result["copied_rows"], result["excluded_rows"]
result["error"] = f"copied {copied} and excluded {excluded} of {source_rows} source rows"
else:
result["status"] = "complete"
return result
@@ -652,11 +646,8 @@ def _copy_table_salvage(
def _state_meta_result(source_rows: Optional[int], **extra: Any) -> dict[str, Any]:
return {
"source_meta_rows": source_rows,
"copied_rows": 0,
"columns": ["key", "value"],
"excluded_keys": sorted(_GENERATED_META_KEYS),
**extra,
"source_meta_rows": source_rows, "copied_rows": 0, "columns": ["key", "value"],
"excluded_keys": sorted(_GENERATED_META_KEYS), **extra,
}
@@ -844,17 +835,13 @@ def _verify_structure(conn: sqlite3.Connection, verification: dict[str, Any]) ->
if verification["schema_version"] != SCHEMA_VERSION:
errors.append(f"schema version is {verification['schema_version']}, expected {SCHEMA_VERSION}")
meta = {
str(row[0]): row[1]
for row in conn.execute("SELECT key, value FROM state_meta WHERE key LIKE 'fts_%'").fetchall()
}
meta = dict(conn.execute("SELECT key, value FROM state_meta WHERE key LIKE 'fts_%'").fetchall())
meta = {str(key): value for key, value in meta.items()}
verification["fts_meta"] = meta
if meta.get("fts_storage_version") != str(FTS_STORAGE_VERSION):
errors.append("fresh FTS storage version was not established")
pending_keys = sorted(
key
for key in _GENERATED_META_KEYS
if key.startswith("fts_") and key != "fts_storage_version" and key in meta
key for key in _GENERATED_META_KEYS if key.startswith("fts_") and key != "fts_storage_version" and key in meta
)
verification["pending_fts_keys"] = pending_keys
if pending_keys:
@@ -890,10 +877,7 @@ def _verify_row_counts(
else:
verification["errors"].append(message)
counts: dict[str, int] = {}
for table in _INVENTORY_TABLES:
if _table_columns(conn, table):
counts[table] = _count_rows(conn, table)
counts = {table: _count_rows(conn, table) for table in _INVENTORY_TABLES if _table_columns(conn, table)}
verification["table_counts"] = counts
for table in ("sessions", "messages", *_AUXILIARY_TABLES):
@@ -908,25 +892,18 @@ def _verify_row_counts(
# A wholly unreadable sessions b-tree is recoverable when every output parent was rebuilt from
# the surviving messages and none were dropped: data loss, but not structural failure.
sessions_fully_reconstructed = bool(
rebuilt_sessions > 0
and counts.get("sessions") == rebuilt_sessions
and counts.get("messages") == retained_messages
and removed_messages == 0
rebuilt_sessions > 0 and counts.get("sessions") == rebuilt_sessions
and counts.get("messages") == retained_messages and removed_messages == 0
)
for table, table_report in copy_report.items():
status = table_report.get("status")
if status not in {"failed", "partial"}:
continue
flag(
f"{table} copy status is {status}",
soft=allow_partial
and (
status == "partial"
or table not in {"sessions", "messages"}
or (table == "sessions" and sessions_fully_reconstructed)
),
tolerable = (
status == "partial" or table not in {"sessions", "messages"}
or (table == "sessions" and sessions_fully_reconstructed)
)
flag(f"{table} copy status is {status}", soft=allow_partial and tolerable)
if orphan_cleanup:
orphan_count = int(orphan_cleanup.get("total_removed_or_relinked") or 0)
@@ -935,10 +912,8 @@ def _verify_row_counts(
if rebuilt_sessions:
# Not a clean recovery: the conversation text survived but its session metadata did not.
flag(
f"{rebuilt_sessions} session(s) could not be salvaged and "
f"were reconstructed as placeholders to retain "
f"{retained_messages} message(s); their metadata (title, model, "
"timestamps, cost) is lost",
f"{rebuilt_sessions} session(s) could not be salvaged and were reconstructed as placeholders to "
f"retain {retained_messages} message(s); their metadata (title, model, timestamps, cost) is lost",
soft=True,
)
@@ -1009,22 +984,19 @@ def _recover_via_lost_and_found(
run_cli_lost_and_found_recover, stub_missing_parent_sessions,
)
missing = ", ".join(missing_required)
sqlite3_bin = find_sqlite3_cli()
if sqlite3_bin is None:
raise SessionRecoverySourceError(
"Partial recovery still requires readable table schemas for: "
+ ", ".join(missing_required)
+ ". " + SQLITE3_CLI_GUIDANCE
f"Partial recovery still requires readable table schemas for: {missing}. {SQLITE3_CLI_GUIDANCE}"
)
lf_path = snapshot_dir / "lost_and_found.db"
try:
cli_report = run_cli_lost_and_found_recover(snapshot_source, lf_path, sqlite3_bin)
except (LostAndFoundError, OSError) as exc:
raise SessionRecoverySourceError(
"Partial recovery could not read the table schemas for: "
+ ", ".join(missing_required)
+ f", and page-level .recover salvage failed: {exc}"
f"Partial recovery could not read the table schemas for: {missing}, "
f"and page-level .recover salvage failed: {exc}"
) from exc
lf_conn = sqlite3.connect(str(lf_path), isolation_level=None)