Squashed integration of the user-facing message audit for this surface set. Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
"""Plain-language copy for "session storage is unavailable / could not be written" notices.
|
|
|
|
One table keyed by ``classify_persistence_error``'s cause bucket feeds every surface (CLI banner,
|
|
gateway home-channel warning, TUI/Desktop RPC errors) so they agree on what happened, what to do,
|
|
and a machine-readable ``code`` a GUI can attach a "Run doctor" button to.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from hermes_state_errors import classify_persistence_error, is_disk_full_error
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StorageFailure:
|
|
cause: str # classify_persistence_error bucket
|
|
code: str # machine-readable, stable: storage_locked | storage_readonly | storage_corrupt | disk_full | ...
|
|
gloss: str # what happened, one clause, lowercase start
|
|
action: str # what to do, one sentence naming the exact command
|
|
|
|
|
|
_DOCTOR = "Run `hermes doctor --fix` to diagnose and repair."
|
|
|
|
# cause -> (code, gloss, action). "disk" is split by is_disk_full_error at lookup time.
|
|
_STORAGE_FAILURES: dict[str, tuple[str, str, str]] = {
|
|
"locked": (
|
|
"storage_locked",
|
|
"the session database is locked by another Hermes process",
|
|
"Wait a moment and try again; if it persists, stop the other Hermes process (`hermes gateway stop`).",
|
|
),
|
|
"disk_full": (
|
|
"disk_full",
|
|
"the disk is full",
|
|
"Free some disk space, then try again.",
|
|
),
|
|
"disk": (
|
|
"storage_readonly",
|
|
"the session database file is read-only or not writable",
|
|
_DOCTOR,
|
|
),
|
|
"corrupt": (
|
|
"storage_corrupt",
|
|
"the session database file is damaged",
|
|
f"{_DOCTOR} Recovery: `hermes sessions recover --source <state.db> --inspect-only`.",
|
|
),
|
|
"fts_index": (
|
|
"storage_index_corrupt",
|
|
"the session search index is damaged (the messages themselves are intact)",
|
|
"Run `hermes doctor --fix` (or `hermes sessions repair`) to rebuild it.",
|
|
),
|
|
"replaced": (
|
|
"storage_replaced",
|
|
"the session database file was replaced while Hermes was running",
|
|
"Stop Hermes (`hermes gateway stop`), run `hermes doctor`, then start it again.",
|
|
),
|
|
"deleted_wal": (
|
|
"storage_replaced",
|
|
"the session database file was changed or replaced while Hermes was running",
|
|
"Stop Hermes (`hermes gateway stop`), run `hermes doctor`, then start it again.",
|
|
),
|
|
"compression": (
|
|
"storage_busy",
|
|
"another process is compressing this session",
|
|
"Send your message again once compression finishes.",
|
|
),
|
|
"compression_closed": (
|
|
"storage_session_rotated",
|
|
"this session was rotated by context compression",
|
|
"Refresh the client (or start a new turn) and send your message again.",
|
|
),
|
|
"turn_lease": (
|
|
"storage_busy",
|
|
"another Hermes process took over this session",
|
|
"Wait for it to finish, then send your message again.",
|
|
),
|
|
"unknown": (
|
|
"storage_unavailable",
|
|
"the session database could not be opened",
|
|
_DOCTOR,
|
|
),
|
|
}
|
|
|
|
|
|
def describe_storage_failure(exc_or_str) -> StorageFailure:
|
|
"""Plain-language description of a persistence failure (never raises)."""
|
|
cause = classify_persistence_error(exc_or_str)
|
|
key = "disk_full" if cause == "disk" and is_disk_full_error(exc_or_str) else cause
|
|
code, gloss, action = _STORAGE_FAILURES.get(key, _STORAGE_FAILURES["unknown"])
|
|
return StorageFailure(cause=cause, code=code, gloss=gloss, action=action)
|
|
|
|
|
|
def storage_failure_details(exc_or_str, limit: int = 200) -> str:
|
|
"""Raw cause for a trailing, secondary "Details:" line (never the lead sentence)."""
|
|
text = " ".join(str(exc_or_str or "").split())
|
|
return text if len(text) <= limit else text[: limit - 3].rstrip() + "..."
|