Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
bfaf859
1
Parent(s): 9174d25
Claude Code: Fix stale 'unknown' error strings - convert to null
Browse files- Add housekeeping function to convert 'unknown' string errors to null
- Fix read_current_stage to auto-repair status files with 'unknown' error
- Ensure healthy agents always have error=null, not error='unknown'
This fixes the "Error: unknown" display issue where the error field
was being set to the string 'unknown' instead of null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
openclaw/.openclaw/health_monitor.py
CHANGED
|
@@ -802,34 +802,70 @@ def reset_health_monitor():
|
|
| 802 |
_global_monitor = None
|
| 803 |
|
| 804 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
def _find_status_file() -> Path:
|
| 806 |
"""Dynamically locate cain_status.json using multiple fallback strategies."""
|
| 807 |
# Strategy 1: CAIN_STATUS_PATH (set by app.py, highest priority)
|
| 808 |
env_path = os.environ.get("CAIN_STATUS_PATH")
|
| 809 |
if env_path:
|
| 810 |
-
|
|
|
|
|
|
|
| 811 |
|
| 812 |
# Strategy 2: CAIN_STATUS_FILE (legacy, for compatibility)
|
| 813 |
env_path = os.environ.get("CAIN_STATUS_FILE")
|
| 814 |
if env_path:
|
| 815 |
-
|
|
|
|
|
|
|
| 816 |
|
| 817 |
# Strategy 3: OPENCLAW_DATA_DIR + cain_status.json
|
| 818 |
data_dir = os.environ.get("OPENCLAW_DATA_DIR")
|
| 819 |
if data_dir:
|
| 820 |
data_path = Path(data_dir) / "cain_status.json"
|
| 821 |
if data_path.exists():
|
|
|
|
| 822 |
return data_path
|
| 823 |
|
| 824 |
# Strategy 4: /data/cain_status.json (Docker volume mount)
|
| 825 |
data_path = Path("/data/cain_status.json")
|
| 826 |
if data_path.exists():
|
|
|
|
| 827 |
return data_path
|
| 828 |
|
| 829 |
# Strategy 5: Relative to health_monitor.py (in .openclaw/ directory)
|
| 830 |
base_dir = Path(__file__).parent.resolve()
|
| 831 |
relative_path = base_dir / "agents" / "cain_status.json"
|
| 832 |
if relative_path.exists():
|
|
|
|
| 833 |
return relative_path
|
| 834 |
|
| 835 |
# Strategy 6: From /app root (Docker) or workspace root (local)
|
|
@@ -838,6 +874,7 @@ def _find_status_file() -> Path:
|
|
| 838 |
workspace_root = workspace_root.parent
|
| 839 |
root_path = workspace_root / "openclaw" / ".openclaw" / "agents" / "cain_status.json"
|
| 840 |
if root_path.exists():
|
|
|
|
| 841 |
return root_path
|
| 842 |
|
| 843 |
# Strategy 7: Fall back to /data/cain_status.json (default for Docker)
|
|
@@ -858,12 +895,20 @@ def read_current_stage() -> str:
|
|
| 858 |
error = data.get('error')
|
| 859 |
stage = data.get('stage', data.get('current_state', 'UNKNOWN'))
|
| 860 |
|
| 861 |
-
#
|
| 862 |
-
# Only append
|
| 863 |
-
|
| 864 |
-
if error and isinstance(error, str) and error.strip() and error.strip().lower() != "unknown":
|
| 865 |
return f"{stage}_ERROR"
|
| 866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
return stage
|
| 868 |
except Exception as e:
|
| 869 |
# Log the actual error for debugging
|
|
|
|
| 802 |
_global_monitor = None
|
| 803 |
|
| 804 |
|
| 805 |
+
def _fix_stale_error_field(status_file: Path) -> bool:
|
| 806 |
+
"""Fix stale 'unknown' error string values - convert to null."""
|
| 807 |
+
try:
|
| 808 |
+
if not status_file.exists():
|
| 809 |
+
return False
|
| 810 |
+
|
| 811 |
+
with open(status_file, "r") as f:
|
| 812 |
+
data = json.load(f)
|
| 813 |
+
|
| 814 |
+
# Check if error field is the string "unknown" (not null)
|
| 815 |
+
error = data.get("error")
|
| 816 |
+
needs_fix = (
|
| 817 |
+
isinstance(error, str) and
|
| 818 |
+
error.strip().lower() == "unknown"
|
| 819 |
+
)
|
| 820 |
+
|
| 821 |
+
if needs_fix:
|
| 822 |
+
# Fix: set error to null
|
| 823 |
+
data["error"] = None
|
| 824 |
+
data["_fixed"] = "converted 'unknown' string to null"
|
| 825 |
+
with open(status_file, "w") as f:
|
| 826 |
+
json.dump(data, f, indent=2)
|
| 827 |
+
return True
|
| 828 |
+
|
| 829 |
+
return False
|
| 830 |
+
except Exception:
|
| 831 |
+
return False
|
| 832 |
+
|
| 833 |
+
|
| 834 |
def _find_status_file() -> Path:
|
| 835 |
"""Dynamically locate cain_status.json using multiple fallback strategies."""
|
| 836 |
# Strategy 1: CAIN_STATUS_PATH (set by app.py, highest priority)
|
| 837 |
env_path = os.environ.get("CAIN_STATUS_PATH")
|
| 838 |
if env_path:
|
| 839 |
+
status_path = Path(env_path)
|
| 840 |
+
_fix_stale_error_field(status_path)
|
| 841 |
+
return status_path
|
| 842 |
|
| 843 |
# Strategy 2: CAIN_STATUS_FILE (legacy, for compatibility)
|
| 844 |
env_path = os.environ.get("CAIN_STATUS_FILE")
|
| 845 |
if env_path:
|
| 846 |
+
status_path = Path(env_path)
|
| 847 |
+
_fix_stale_error_field(status_path)
|
| 848 |
+
return status_path
|
| 849 |
|
| 850 |
# Strategy 3: OPENCLAW_DATA_DIR + cain_status.json
|
| 851 |
data_dir = os.environ.get("OPENCLAW_DATA_DIR")
|
| 852 |
if data_dir:
|
| 853 |
data_path = Path(data_dir) / "cain_status.json"
|
| 854 |
if data_path.exists():
|
| 855 |
+
_fix_stale_error_field(data_path)
|
| 856 |
return data_path
|
| 857 |
|
| 858 |
# Strategy 4: /data/cain_status.json (Docker volume mount)
|
| 859 |
data_path = Path("/data/cain_status.json")
|
| 860 |
if data_path.exists():
|
| 861 |
+
_fix_stale_error_field(data_path)
|
| 862 |
return data_path
|
| 863 |
|
| 864 |
# Strategy 5: Relative to health_monitor.py (in .openclaw/ directory)
|
| 865 |
base_dir = Path(__file__).parent.resolve()
|
| 866 |
relative_path = base_dir / "agents" / "cain_status.json"
|
| 867 |
if relative_path.exists():
|
| 868 |
+
_fix_stale_error_field(relative_path)
|
| 869 |
return relative_path
|
| 870 |
|
| 871 |
# Strategy 6: From /app root (Docker) or workspace root (local)
|
|
|
|
| 874 |
workspace_root = workspace_root.parent
|
| 875 |
root_path = workspace_root / "openclaw" / ".openclaw" / "agents" / "cain_status.json"
|
| 876 |
if root_path.exists():
|
| 877 |
+
_fix_stale_error_field(root_path)
|
| 878 |
return root_path
|
| 879 |
|
| 880 |
# Strategy 7: Fall back to /data/cain_status.json (default for Docker)
|
|
|
|
| 895 |
error = data.get('error')
|
| 896 |
stage = data.get('stage', data.get('current_state', 'UNKNOWN'))
|
| 897 |
|
| 898 |
+
# CRITICAL: Treat null, None, empty string, or "unknown" string as HEALTHY
|
| 899 |
+
# Only append _ERROR if there's an actual error message (non-empty, not "unknown")
|
| 900 |
+
if error and isinstance(error, str) and error.strip() and error.strip().lower() not in ("", "unknown", "none"):
|
|
|
|
| 901 |
return f"{stage}_ERROR"
|
| 902 |
|
| 903 |
+
# Healthy state - fix status file if error is "unknown" string instead of null
|
| 904 |
+
if isinstance(error, str) and error.strip().lower() == "unknown":
|
| 905 |
+
try:
|
| 906 |
+
data["error"] = None
|
| 907 |
+
with open(status_file, "w") as f:
|
| 908 |
+
json.dump(data, f, indent=2)
|
| 909 |
+
except Exception:
|
| 910 |
+
pass # Best effort fix
|
| 911 |
+
|
| 912 |
return stage
|
| 913 |
except Exception as e:
|
| 914 |
# Log the actual error for debugging
|