Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/clawops/recovery/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
type RunCommandFunc = Callable[..., ExecResult]
type TarWriter = Callable[
[pathlib.Path],
None,
dict[str, object],
]


Expand All @@ -21,7 +21,9 @@ class BackupBackend(Protocol):

name: str

def create(self, archive_tmp_path: pathlib.Path) -> tuple[bool, str | None]:
def create(
self, archive_tmp_path: pathlib.Path
) -> tuple[bool, str | None, dict[str, object] | None]:
"""Create one archive and return status + optional failure reason."""
...

Expand All @@ -39,16 +41,18 @@ def is_available(self) -> bool:
"""Return whether the OpenClaw executable is available."""
return self._which("openclaw") is not None

def create(self, archive_tmp_path: pathlib.Path) -> tuple[bool, str | None]:
def create(
self, archive_tmp_path: pathlib.Path
) -> tuple[bool, str | None, dict[str, object] | None]:
"""Create one archive through OpenClaw."""
result = self._run_command(
["openclaw", "backup", "create", str(archive_tmp_path)],
timeout_seconds=600,
)
if result.ok:
return True, None
return True, None, None
detail = result.stderr.strip() or result.stdout.strip() or "openclaw backup create failed"
return False, detail
return False, detail, None


class TarBackupBackend:
Expand All @@ -59,7 +63,9 @@ class TarBackupBackend:
def __init__(self, *, writer: TarWriter) -> None:
self._writer = writer

def create(self, archive_tmp_path: pathlib.Path) -> tuple[bool, str | None]:
def create(
self, archive_tmp_path: pathlib.Path
) -> tuple[bool, str | None, dict[str, object] | None]:
"""Create one archive through the fallback tar writer."""
self._writer(archive_tmp_path)
return True, None
manifest = self._writer(archive_tmp_path)
return True, None, manifest
1 change: 1 addition & 0 deletions src/clawops/recovery/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ class BackupCreateExecution:
mode: str | None = None
fallback_used: bool = False
fallback_reason: str | None = None
manifest: dict[str, object] | None = None
8 changes: 5 additions & 3 deletions src/clawops/recovery/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from clawops.strongclaw_runtime import CommandError, ExecResult

type RunCommandFunc = Callable[..., ExecResult]
type TarWriter = Callable[[pathlib.Path], None]
type TarWriter = Callable[[pathlib.Path], dict[str, object]]
type SafeUnlink = Callable[[pathlib.Path], None]


Expand Down Expand Up @@ -49,7 +49,7 @@ def create_backup_execution(
fallback_reason: str | None = None
if openclaw_backend.is_available():
safe_unlink(archive_tmp_path)
backend_ok, backend_error = openclaw_backend.create(archive_tmp_path)
backend_ok, backend_error, _ = openclaw_backend.create(archive_tmp_path)
if backend_ok:
archive_tmp_path.replace(archive_path)
return BackupCreateExecution(
Expand All @@ -65,8 +65,9 @@ def create_backup_execution(

safe_unlink(archive_tmp_path)
fallback_backend = TarBackupBackend(writer=tar_writer)
fallback_manifest: dict[str, object] | None = None
try:
fallback_backend.create(archive_tmp_path)
_, _, fallback_manifest = fallback_backend.create(archive_tmp_path)
archive_tmp_path.replace(archive_path)
except (OSError, tarfile.TarError) as exc:
safe_unlink(archive_tmp_path)
Expand All @@ -79,4 +80,5 @@ def create_backup_execution(
mode="tar-fallback",
fallback_used=fallback_reason is not None,
fallback_reason=fallback_reason,
manifest=fallback_manifest,
)
4 changes: 3 additions & 1 deletion src/clawops/recovery/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

from collections.abc import Mapping

from clawops.observability import TelemetryValue

def event_payload(event: str, fields: Mapping[str, object]) -> dict[str, object]:

def event_payload(event: str, fields: Mapping[str, TelemetryValue]) -> dict[str, TelemetryValue]:
"""Build one structured recovery telemetry payload."""
return {"event": event, **dict(fields)}
Loading
Loading