From f564996a8f0ea9da7d7308b8b4dd5b416dcf3d87 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Fri, 10 Apr 2026 13:19:06 -0700 Subject: [PATCH] feat(system): S85: Trigger self-healing live + drone error handler + watchdog v1.1 snapshot + STATUS Co-Authored-By: @devpulse --- src/aipass/devpulse/.aipass/aipass_local_prompt.md | 4 ++-- src/aipass/drone/apps/drone.py | 4 ++++ src/aipass/prax/CLOSED_PLANS.local.json | 7 +++++++ src/aipass/spawn/tests/test_file_ops.py | 7 +++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/aipass/devpulse/.aipass/aipass_local_prompt.md b/src/aipass/devpulse/.aipass/aipass_local_prompt.md index 4d3a2e7b..fd2f17a9 100644 --- a/src/aipass/devpulse/.aipass/aipass_local_prompt.md +++ b/src/aipass/devpulse/.aipass/aipass_local_prompt.md @@ -92,11 +92,11 @@ INBOX="path/to/inbox.json"; while true; do sleep 10; UNREAD=$(python3 -c "import # 4. Wake notification arrives → read mail → process → dispatch next → repeat ``` -**Key:** Use `unread_count > 0` (not total_messages — close resets totals). Always `close all` before arming. 10s poll interval. `run_in_background: true` so the completion notification wakes you. On timeout, wake anyway to check if agent crashed — then either restart watchdog or re-dispatch. +**Key:** Snapshot unread_count BEFORE arming, then wake when it increases. Don't require empty inbox — works with existing mail. 10s poll interval. `run_in_background: true` so the completion notification wakes you. On timeout, wake anyway to check if agent crashed. **Watchdog one-liner (copy-paste ready):** ``` -INBOX="/home/patrick/Projects/AIPass/src/aipass/devpulse/.ai_mail.local/inbox.json"; C=0; while [ $C -lt 60 ]; do sleep 10; C=$((C+1)); UNREAD=$(python3 -c "import json; from pathlib import Path; p=Path('$INBOX'); print(json.loads(p.read_text()).get('unread_count',0) if p.exists() else 0)" 2>/dev/null); if [ "$UNREAD" -gt "0" ]; then echo "WOKE: $UNREAD unread"; exit 0; fi; done; echo "TIMEOUT: 10min no reply — check if agent crashed"; exit 0 +INBOX="/home/patrick/Projects/AIPass/src/aipass/devpulse/.ai_mail.local/inbox.json"; INITIAL=$(python3 -c "import json; from pathlib import Path; p=Path('$INBOX'); print(json.loads(p.read_text()).get('unread_count',0) if p.exists() else 0)" 2>/dev/null); C=0; while [ $C -lt 60 ]; do sleep 10; C=$((C+1)); CURRENT=$(python3 -c "import json; from pathlib import Path; p=Path('$INBOX'); print(json.loads(p.read_text()).get('unread_count',0) if p.exists() else 0)" 2>/dev/null); if [ "$CURRENT" -gt "$INITIAL" ]; then echo "WOKE: new mail ($INITIAL→$CURRENT)"; exit 0; fi; done; echo "TIMEOUT: 10min no new mail"; exit 0 ``` ## Memory & Tracking diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index f6da6e73..0b1b7242 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -431,6 +431,10 @@ def main() -> int: logger.warning("Registry error during systems listing: %s", exc) err_console.print(f"drone: {exc}") return 1 + except Exception as exc: + logger.error("[drone] Unhandled error in systems: %s", exc) + err_console.print(f"drone: unexpected error: {exc}") + return 1 # scan — discover available commands in a branch if command == "scan": diff --git a/src/aipass/prax/CLOSED_PLANS.local.json b/src/aipass/prax/CLOSED_PLANS.local.json index a270f65e..b99b3e78 100644 --- a/src/aipass/prax/CLOSED_PLANS.local.json +++ b/src/aipass/prax/CLOSED_PLANS.local.json @@ -55,6 +55,13 @@ "subject": "DPLAN-0039 prax audit work items — test coverage, JSON corruption, stale cleanup", "date_closed": "2026-03-29", "location": "prax" + }, + { + "plan_id": "FPLAN-0097", + "type": "FPLAN", + "subject": "Codex + Gemini log monitoring adapters for prax monitor", + "date_closed": "2026-04-10", + "location": "prax" } ] } diff --git a/src/aipass/spawn/tests/test_file_ops.py b/src/aipass/spawn/tests/test_file_ops.py index 330a8070..f99f923d 100644 --- a/src/aipass/spawn/tests/test_file_ops.py +++ b/src/aipass/spawn/tests/test_file_ops.py @@ -13,6 +13,8 @@ import json from pathlib import Path from unittest.mock import patch +import pytest + from aipass.spawn.apps.handlers.file_ops import ( SKIP_NAMES, _replace_path_placeholders, @@ -68,6 +70,11 @@ class TestEnsureDirectory: assert target.exists() assert target.is_dir() + def test_ensure_directory_none_raises_valueerror(self) -> None: + """Passing None raises ValueError with clear message.""" + with pytest.raises(ValueError, match="ensure_directory received None path"): + ensure_directory(None) + # --------------------------------------------------------------------------- # copy_template