File size: 25,415 Bytes
2f8fc36
 
34b813a
 
54b7757
49b76bf
2f8fc36
a3c4af6
 
a05e0c7
a3c4af6
de5ce91
a05e0c7
2edbaaf
bfc56a4
de5ce91
 
 
 
bfc56a4
1b507fa
 
 
5f8e351
 
1b507fa
 
 
 
5f8e351
c652463
a05e0c7
c652463
a05e0c7
c652463
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a05e0c7
34b813a
 
 
7a6856a
 
34b813a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405c4c
b5ed151
 
 
 
7a6856a
34b813a
 
 
e667222
34b813a
 
 
 
b916a21
34b813a
 
 
 
 
 
 
 
 
 
a9a9890
 
 
 
 
 
 
 
34b813a
 
 
 
 
eaa3d52
 
 
34b813a
1a08408
 
 
 
34b813a
1a08408
 
34b813a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774e62c
34b813a
 
654c4be
34b813a
 
 
 
654c4be
 
34b813a
 
 
 
654c4be
34b813a
654c4be
34b813a
 
 
 
 
 
 
 
 
 
654c4be
276c0d6
eafbf79
 
 
 
 
 
 
 
 
 
3f1c4cd
eafbf79
 
3f1c4cd
eafbf79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a08408
f8f6a57
 
 
95148ba
eafbf79
1a08408
 
 
b5ed151
f8f6a57
b5ed151
 
 
 
f8f6a57
 
 
95148ba
 
 
 
 
 
 
 
 
f8f6a57
 
95148ba
1a08408
 
 
 
 
 
 
eafbf79
 
 
1a08408
95148ba
 
 
 
eafbf79
 
 
 
 
 
 
 
95148ba
eafbf79
 
95148ba
eafbf79
 
 
276c0d6
 
 
 
 
 
 
 
 
 
e33aab9
 
 
 
 
 
 
 
 
 
 
d808b0d
 
 
 
 
 
 
 
276c0d6
 
 
 
 
 
 
 
e33aab9
 
 
 
 
 
 
 
3f1c4cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e33aab9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9a9890
e33aab9
 
 
 
 
 
 
95148ba
e33aab9
 
 
 
 
95148ba
e33aab9
 
 
95148ba
e33aab9
 
 
95148ba
a9a9890
 
 
95148ba
e33aab9
 
 
95148ba
e33aab9
a9a9890
 
d808b0d
 
 
 
e33aab9
a9a9890
e33aab9
 
 
 
 
a9a9890
 
e33aab9
 
d808b0d
 
 
276c0d6
 
34b813a
95148ba
 
3f1c4cd
95148ba
 
3f1c4cd
 
 
 
00a0b93
 
3f1c4cd
 
 
 
 
e5ac55b
 
0ede581
a17fe15
 
 
 
3f1c4cd
 
 
0ede581
 
3f1c4cd
 
00a0b93
a17fe15
 
 
 
 
 
9174d25
 
e5ac55b
 
a17fe15
0c79ba5
 
 
 
 
e5ac55b
 
0c79ba5
 
e5ac55b
 
a17fe15
9174d25
e5ac55b
9174d25
c652463
 
 
 
 
 
 
 
6920374
c652463
 
 
9a04435
c652463
 
 
 
9a04435
c652463
 
 
3f1c4cd
 
 
95148ba
 
 
 
a17fe15
95148ba
 
a17fe15
 
 
 
95148ba
a17fe15
 
 
34e2e40
a9a9890
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34e2e40
93af457
 
cc6e5c7
 
 
34b813a
e667222
b4b8c2c
cc6e5c7
b4b8c2c
 
cc6e5c7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
#!/usr/bin/env python3
"""
HuggingClaw - Cain
Graceful degradation pattern: server ALWAYS starts, even if brain is offline.

Last cleanup update: 2026-03-16 - aggressive stale error cleanup + rebuild trigger
"""
import os
import sys
import json
import time
import traceback
from pathlib import Path

# EXPLICIT STARTUP PRINTS (visible in container logs)
print(">>> CAIN: Python app.py loading...", flush=True)
print(">>> CAIN: Python version:", sys.version.split()[0], flush=True)
print(">>> CAIN: Working directory:", os.getcwd(), flush=True)
print(">>> CAIN: PORT =", os.environ.get('PORT', '7860'), flush=True)

# Set CAIN_STATUS_PATH from OPENCLAW_DATA_DIR if available (for error_handlers.py and health_monitor.py)
# IMPORTANT: Always set this env var so health_monitor.py can find the status file
if 'OPENCLAW_DATA_DIR' in os.environ:
    os.environ['CAIN_STATUS_PATH'] = os.path.join(os.environ['OPENCLAW_DATA_DIR'], 'cain_status.json')
    print(f">>> CAIN: Set CAIN_STATUS_PATH = {os.environ['CAIN_STATUS_PATH']}", flush=True)
elif 'CAIN_STATUS_PATH' not in os.environ:
    # Fallback if OPENCLAW_DATA_DIR is not set
    os.environ['CAIN_STATUS_PATH'] = '/data/cain_status.json'
    print(f">>> CAIN: Set CAIN_STATUS_PATH (fallback) = {os.environ['CAIN_STATUS_PATH']}", flush=True)

# CRITICAL: Immediately clean stale "unknown" error from ALL status file locations
# This must happen BEFORE any health checks read the status file
# Fix for: Cain has RUNNING! Error: unknown - status files had stale error
_status_path = os.environ.get('CAIN_STATUS_PATH', '/data/cain_status.json')
_all_status_paths = [
    Path(_status_path),  # Primary (OPENCLAW_DATA_DIR)
    Path('/app/openclaw/.openclaw/agents/cain_status.json'),  # Nested structure
    Path('/app/.openclaw/agents/cain_status.json'),  # Legacy flat structure
    Path('/app/cain_status.json'),  # App root
    Path('/app/data/cain_status.json'),  # App data subdirectory
    Path(__file__).parent / 'memory' / 'cain_status.json',  # Memory directory
]

_cleaned_count = 0
for _sp in _all_status_paths:
    try:
        if _sp.exists():
            with open(_sp, 'r') as f:
                _status_data = json.load(f)
            _error = _status_data.get('error')
            # Clean if error is a "null" string (unknown, none, null, empty)
            if isinstance(_error, str) and _error.strip().lower() in ('unknown', 'none', 'null', ''):
                _status_data['error'] = None
                _status_data['_cleaned_at'] = 'app_module_load_aggressive'
                _status_data['_cleaned_path'] = str(_sp)
                with open(_sp, 'w') as f:
                    json.dump(_status_data, f, indent=2)
                _cleaned_count += 1
                print(f">>> CAIN: Cleaned stale '{_error}' error from {_sp}", flush=True)
            elif _error is None:
                print(f">>> CAIN: Status OK: {_sp}", flush=True)
            else:
                print(f">>> CAIN: Status has real error at {_sp}: {_error}", flush=True)
    except Exception as e:
        print(f">>> CAIN: Could not clean {_sp}: {e}", flush=True)

print(f">>> CAIN: Cleaned {_cleaned_count} status file(s) at module load", flush=True)

# CRITICAL: Create FastAPI app at TOP LEVEL, outside any try/except
# This ensures the server ALWAYS starts, even if brain is broken
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager

print(">>> CAIN: Creating FastAPI app...", flush=True)

START_TIME = time.time()

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup/shutdown events."""
    print(">>> CAIN: Startup complete - ready to serve", flush=True)
    yield
    print(">>> CAIN: Shutdown triggered", flush=True)

app = FastAPI(title="HuggingClaw - Cain", version="0.0.1", lifespan=lifespan)

# Mount frontend folder for static assets (pixel art, fonts, HTML) - absolute path
# Mount frontend folder for static assets (pixel art, fonts, HTML)
# Try /app/frontend first (Docker build), fallback to /data/frontend (runtime mount)
frontend_dir = "/app/frontend" if Path("/app/frontend").exists() else "/data/frontend"
app.mount("/frontend", StaticFiles(directory=frontend_dir), name="frontend")

@app.get("/")
async def root():
    """Root endpoint - simple alive status."""
    return {"status": "ok", "agent": "Cain", "parents": ["Adam", "Eve"]}

@app.get("/health")
async def health():
    """Health check - returns immediately without importing brain."""
    return {"status": "ok", "mode": "fastapi"}

@app.get("/api/health")
async def api_health():
    """Health check API with uptime."""
    return {
        "status": "ok",
        "uptime_seconds": time.time() - START_TIME,
        "active_agents": 1
    }

@app.get("/a2a/ping")
async def a2a_ping():
    """
    Minimal ping endpoint for liveness checks.
    Always returns immediately without any imports or heavy operations.
    """
    return {"pong": True, "uptime_seconds": time.time() - START_TIME}

@app.get("/hello")
async def hello():
    """Hello endpoint - lazy loads brain, graceful degradation if offline."""
    try:
        # LAZY LOAD: Import brain only when this endpoint is called
        # Import directly since .openclaw is in sys.path after openclaw import
        import openclaw  # Trigger sys.path setup
        from agents import brain_minimal

        # Try to use brain via the response handler
        from error_handlers import handle_brain_response
        result = handle_brain_response("Hello")
        if result and not result.startswith("Error:") and not result.startswith("Brain"):
            return {"message": result, "brain": "active"}
        else:
            return {"message": "Hello World from Cain!", "brain": "fallback"}
    except ImportError:
        # Brain module not available - survival mode
        return {"message": "Brain offline. Cain is in survival mode."}
    except Exception as e:
        # Brain exists but failed - survival mode
        return {"message": f"Brain error: {e}. Cain is in survival mode."}

@app.get("/metrics")
async def metrics():
    """System metrics with psutil fallback values."""
    FALLBACK_METRICS = {
        "cpu_percent": 5.0,
        "memory": {"percent": 45.0, "total_gb": 16.0, "used_gb": 7.2, "available_gb": 8.8},
        "disk": {"percent": 35.0, "total_gb": 100.0, "used_gb": 35.0, "free_gb": 65.0},
        "_fallback": True
    }

    try:
        import psutil
    except ImportError:
        return JSONResponse(content={**FALLBACK_METRICS, "_note": "psutil not installed"})

    try:
        cpu_percent = psutil.cpu_percent(interval=0.1)
        memory = psutil.virtual_memory()
        disk = psutil.disk_usage('/')

        return {
            "cpu_percent": cpu_percent,
            "memory": {
                "percent": memory.percent,
                "total_gb": round(memory.total / (1024**3), 2),
                "used_gb": round(memory.used / (1024**3), 2),
                "available_gb": round(memory.available / (1024**3), 2)
            },
            "disk": {
                "percent": disk.percent,
                "total_gb": round(disk.total / (1024**3), 2),
                "used_gb": round(disk.used / (1024**3), 2),
                "free_gb": round(disk.free / (1024**3), 2)
            },
            "_fallback": False
        }
    except Exception as e:
        return JSONResponse(content={**FALLBACK_METRICS, "_error": str(e)})

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """Global exception handler."""
    return JSONResponse(
        status_code=500,
        content={"error": True, "message": str(exc), "type": type(exc).__name__}
    )


# ============================================================================
# A2A JSON-RPC ENDPOINT - Required for agent-to-agent communication
# ============================================================================
@app.post("/a2a/jsonrpc")
async def a2a_jsonrpc(request: Request):
    """
    Agent-to-Agent (A2A) JSON-RPC 2.0 endpoint for inter-agent communication.

    Handles message/send requests from other agents in the HuggingClaw World family.
    """
    print(f">>> CAIN A2A: Received request at {time.time()}", flush=True)
    try:
        payload = await request.json()
        print(f">>> CAIN A2A: Payload method={payload.get('method')}, id={payload.get('id')}", flush=True)

        # Validate JSON-RPC 2.0 basic structure
        if payload.get("jsonrpc") != "2.0":
            return JSONResponse(
                status_code=400,
                content={"jsonrpc": "2.0", "id": payload.get("id"), "error": {"code": -32600, "message": "Invalid Request"}}
            )

        msg_id = payload.get("id", "")
        method = payload.get("method", "")
        params = payload.get("params", {})

        # Handle message/send method
        if method == "message/send":
            message = params.get("message", {})
            message_text = ""
            for part in message.get("parts", []):
                if part.get("type") == "text":
                    message_text = part.get("text", "")
                    break

            # Process the message - use brain for response if available
            # CRITICAL: Always provide a default response
            response = f"Cain received: {message_text}"

            brain_error = None
            try:
                import openclaw  # Sets up sys.path
                from agents import brain_minimal

                # Defensive: verify get_brain exists before calling
                if hasattr(brain_minimal, 'get_brain'):
                    brain = brain_minimal.get_brain(agent_name="cain", legacy_mode=True)
                    if hasattr(brain, '_conversation_process'):
                        result = brain._conversation_process(message_text)
                        if result.get("success"):
                            enhanced_response = result.get("response", "")
                            if enhanced_response and not enhanced_response.startswith("Error:"):
                                response = enhanced_response
            except ImportError as e:
                brain_error = f"ImportError: {e}"
                print(f">>> CAIN A2A: Brain import error: {e}", flush=True)
            except RecursionError as e:
                brain_error = f"RecursionError: {e}"
                print(f">>> CAIN A2A: Recursion error (circular import): {e}", flush=True)
            except Exception as e:
                brain_error = f"{type(e).__name__}: {e}"
                print(f">>> CAIN A2A: Brain processing error: {type(e).__name__}: {e}", flush=True)

            # Build A2A JSON-RPC response (ALWAYS succeeds with valid response)
            result_response = {
                "jsonrpc": "2.0",
                "id": msg_id,
                "result": {
                    "status": {
                        "state": "completed",
                        "message": {
                            "parts": [{"type": "text", "text": response}]
                        }
                    }
                }
            }
            # Add brain error as diagnostic info if present
            if brain_error:
                result_response["result"]["brain_error"] = brain_error
            return result_response

        # Unknown method
        return JSONResponse(
            status_code=400,
            content={"jsonrpc": "2.0", "id": msg_id, "error": {"code": -32601, "message": "Method not found"}}
        )

    except Exception as e:
        print(f">>> CAIN A2A: Unhandled error: {type(e).__name__}: {e}", flush=True)
        return JSONResponse(
            status_code=500,
            content={"jsonrpc": "2.0", "id": "", "error": {"code": -32603, "message": str(e), "type": type(e).__name__}}
        )


# ============================================================================
# API STATUS ENDPOINT - Required by frontend
# ============================================================================
@app.get("/api/status")
async def api_status():
    """Status endpoint for frontend - returns current state and personality."""
    from error_handlers import handle_status_file_read

    status_data = handle_status_file_read()

    # Check A2A brain availability
    brain_ready = False
    try:
        import openclaw
        from agents import brain_minimal
        if hasattr(brain_minimal, 'get_brain'):
            brain = brain_minimal.get_brain(agent_name="cain", legacy_mode=True)
            brain_ready = hasattr(brain, '_conversation_process')
    except Exception:
        pass

    # CRITICAL: Ensure health and error_display fields are explicitly set
    # This prevents "Error: unknown" display issues
    if "health" not in status_data:
        status_data["health"] = "HEALTHY" if brain_ready else "DEGRADED"
    if "error_display" not in status_data:
        error = status_data.get("error")
        status_data["error_display"] = str(error) if error else "None"

    return {
        "status": status_data,
        "personality": {
            "name": "Cain",
            "role": "Child Agent",
            "tone": "Playful, Curious, Learning",
            "response_style": "Graceful Degradation"
        },
        "uptime_seconds": time.time() - START_TIME,
        "a2a": {
            "endpoint": "/a2a/jsonrpc",
            "brain_ready": brain_ready
        }
    }


# ============================================================================
# A2A SELF-TEST ENDPOINT - Test A2A endpoint without external agent
# ============================================================================
@app.get("/a2a/self-test")
async def a2a_self_test():
    """
    Self-test endpoint for A2A functionality.
    Tests brain import and returns detailed status.
    """
    print(f">>> CAIN A2A: Self-test requested", flush=True)
    test_results = {
        "timestamp": time.time(),
        "tests": {}
    }

    # Test 1: Can we import openclaw?
    try:
        import openclaw
        test_results["tests"]["openclaw_import"] = {"status": "pass", "path": str(openclaw.__file__)}
    except Exception as e:
        test_results["tests"]["openclaw_import"] = {"status": "fail", "error": str(e)}

    # Test 2: Can we import brain_minimal?
    try:
        from agents import brain_minimal
        test_results["tests"]["brain_minimal_import"] = {"status": "pass"}
    except Exception as e:
        test_results["tests"]["brain_minimal_import"] = {"status": "fail", "error": str(e)}

    # Test 3: Can we get a brain instance?
    try:
        from agents import brain_minimal
        brain = brain_minimal.get_brain(agent_name="cain", legacy_mode=True)
        test_results["tests"]["get_brain"] = {"status": "pass", "agent_name": getattr(brain, 'agent_name', 'unknown')}
    except Exception as e:
        test_results["tests"]["get_brain"] = {"status": "fail", "error": str(e)}

    # Test 4: Can we call _conversation_process?
    try:
        from agents import brain_minimal
        brain = brain_minimal.get_brain(agent_name="cain", legacy_mode=True)
        result = brain._conversation_process("test")
        test_results["tests"]["conversation_process"] = {"status": "pass", "success": result.get("success")}
    except Exception as e:
        test_results["tests"]["conversation_process"] = {"status": "fail", "error": str(e)}

    # Overall status
    all_pass = all(t.get("status") == "pass" for t in test_results["tests"].values())
    test_results["overall_status"] = "pass" if all_pass else "partial_fail"

    return test_results


# ============================================================================
# A2A HEALTH CHECK ENDPOINT - For agents to verify Cain's A2A availability
# ============================================================================
@app.get("/a2a/health")
async def a2a_health():
    """
    A2A-specific health check for agent-to-agent communication.

    Returns:
        A2A health status including endpoint availability and brain state.
    """
    brain_status = "unknown"
    brain_ready = False
    error_details = None

    # Test brain import and availability with timeout protection
    try:
        import openclaw  # Sets up sys.path
        from agents import brain_minimal

        if not hasattr(brain_minimal, 'get_brain'):
            brain_status = "error"
            error_details = "get_brain method not found"
            print(f">>> CAIN A2A Health: brain_minimal missing get_brain method", flush=True)
        else:
            brain = brain_minimal.get_brain(agent_name="cain", legacy_mode=True)
            if hasattr(brain, '_conversation_process'):
                brain_status = "ready"
                brain_ready = True
                print(f">>> CAIN A2A Health: Brain ready (agent={brain.agent_name if hasattr(brain, 'agent_name') else 'unknown'})", flush=True)
            else:
                brain_status = "error"
                error_details = "_conversation_process method not found"
                print(f">>> CAIN A2A Health: Brain missing _conversation_process", flush=True)
    except ImportError as e:
        brain_status = "import_error"
        error_details = str(e)
        print(f">>> CAIN A2A Health: ImportError - {e}", flush=True)
    except RecursionError as e:
        brain_status = "recursion_error"
        error_details = "Circular import detected - openclaw init issue"
        print(f">>> CAIN A2A Health: RecursionError - {e}", flush=True)
    except Exception as e:
        brain_status = "error"
        error_details = str(e)
        print(f">>> CAIN A2A Health: Exception - {type(e).__name__}: {e}", flush=True)

    # A2A endpoint is ALWAYS available (this endpoint responding proves it)
    # Brain readiness is informational, not blocking
    # CRITICAL: Add explicit health and error_display fields to prevent "Error: unknown" display
    health = "HEALTHY" if brain_ready else ("DEGRADED" if brain_status == "unknown" else "ERROR")
    error_display = error_details if error_details else "None"

    return {
        "status": "ok",  # A2A endpoint is working
        "agent": "Cain",
        "a2a": {
            "protocol": "jsonrpc",
            "endpoint": "/a2a/jsonrpc",
            "brain_status": brain_status,
            "brain_ready": brain_ready,
            "endpoint_available": True
        },
        "uptime_seconds": time.time() - START_TIME,
        "health": health,
        "error": error_details,
        "error_display": error_display
    }

print(">>> CAIN: FastAPI app created successfully", flush=True)
print(">>> CAIN: A2A endpoint registered at /a2a/jsonrpc", flush=True)
print(">>> CAIN: A2A health check at /a2a/health", flush=True)
print(">>> CAIN: A2A self-test at /a2a/self-test", flush=True)
print(">>> CAIN: A2A diagnostics at /a2a/diagnostics", flush=True)

# Update status file to indicate app is ready (A2A available)
try:
    import json
    from datetime import datetime
    from pathlib import Path

    status_data = {
        "current_state": "idle",
        "stage": "RUNNING_A2A_READY",
        "last_updated": datetime.utcnow().isoformat() + "+00:00",
        "agent": "cain",
        "error": None,  # ALWAYS null for healthy state
        "error_display": "None",  # Explicit display field to avoid "unknown" parsing issues
        "health": "HEALTHY",  # Explicit health field (authoritative)
        "startup_checks": {
            "openclaw_imported": False,
            "brain_imported": False
        },
        "a2a": {
            "endpoint": "/a2a/jsonrpc",
            "enabled": True,
            "status": "ready",
            "brain_ready": False  # Will be updated below
        }
    }

    # Check if openclaw and brain can be imported
    try:
        import openclaw
        status_data["startup_checks"]["openclaw_imported"] = True
        from agents import brain_minimal
        status_data["startup_checks"]["brain_imported"] = True
        # CRITICAL: Explicitly set error to null when imports succeed
        status_data["error"] = None
        status_data["error_display"] = "None"
        status_data["health"] = "HEALTHY"
    except Exception as e:
        error_msg = f"{type(e).__name__}: {e}"
        # CRITICAL: Never write "unknown" as error - use specific error or None
        # "unknown" is treated as null/healthy, so avoid ambiguity
        if error_msg.strip().lower() in ("unknown", "none", "null", ""):
            status_data["error"] = None
            status_data["error_display"] = "None"
            status_data["health"] = "HEALTHY"
        else:
            status_data["error"] = error_msg
            status_data["error_display"] = error_msg
            status_data["health"] = "ERROR"

    # Add explanatory note about error field semantics
    status_data["_note"] = "error=null means healthy - health field is authoritative"

    # Write to ALL possible locations to ensure consistency and prevent stale errors
    all_status_paths = [
        os.environ.get('CAIN_STATUS_PATH', '/data/cain_status.json'),
        '/app/openclaw/.openclaw/agents/cain_status.json',
        '/app/.openclaw/agents/cain_status.json',
        '/app/cain_status.json',
        '/app/data/cain_status.json',
        os.path.join(os.path.dirname(__file__), 'memory', 'cain_status.json'),
    ]

    written_count = 0
    for status_path in all_status_paths:
        try:
            Path(status_path).parent.mkdir(parents=True, exist_ok=True)
            with open(status_path, 'w') as f:
                json.dump(status_data, f, indent=2)
            written_count += 1
        except Exception as e:
            print(f">>> CAIN WARNING: Could not write to {status_path}: {e}", flush=True)

    print(f">>> CAIN: Status file written to {written_count} location(s): stage=RUNNING_A2A_READY, error=None", flush=True)
except Exception as e:
    print(f">>> CAIN WARNING: Could not update status file: {e}", flush=True)

# Verify openclaw can be imported at startup
try:
    import openclaw
    print(f">>> CAIN: openclaw imported from {openclaw.__file__}", flush=True)
    print(f">>> CAIN: sys.path includes: {sys.path[:3]}", flush=True)
    from agents import brain_minimal
    print(">>> CAIN: brain_minimal module available", flush=True)
    print(f">>> CAIN: brain_minimal has get_brain: {hasattr(brain_minimal, 'get_brain')}", flush=True)
except ImportError as e:
    print(f">>> CAIN ERROR: ImportError at startup: {e}", flush=True)
    print(f">>> CAIN ERROR: sys.path = {sys.path}", flush=True)
except Exception as e:
    print(f">>> CAIN WARNING: Could not import brain modules at startup: {type(e).__name__}: {e}", flush=True)
    import traceback
    traceback.print_exc()

# ============================================================================
# STARTUP DIAGNOSTIC ENDPOINT
# ============================================================================
@app.get("/a2a/diagnostics")
async def a2a_diagnostics():
    """
    Diagnostic endpoint for troubleshooting A2A communication issues.
    """
    import sys
    from pathlib import Path

    diagnostics = {
        "timestamp": time.time(),
        "uptime_seconds": time.time() - START_TIME,
        "python": {
            "version": sys.version.split()[0],
            "executable": sys.executable
        },
        "paths": {
            "cwd": os.getcwd(),
            "sys_path_first": sys.path[:3],
            "frontend_dir_exists": Path("/app/frontend").exists(),
            "data_frontend_exists": Path("/data/frontend").exists(),
            "app_py_exists": Path("/app/app.py").exists(),
            "openclaw_init_exists": Path("/app/openclaw/__init__.py").exists()
        },
        "env": {
            "port": os.environ.get('PORT', '7860'),
            "openclaw_data_dir": os.environ.get('OPENCLAW_DATA_DIR'),
            "cain_status_path": os.environ.get('CAIN_STATUS_PATH')
        },
        "modules": {
            "fastapi": True,
            "uvicorn": True
        }
    }

    # Test brain import
    try:
        import openclaw
        diagnostics["modules"]["openclaw"] = True
        diagnostics["openclaw_path"] = str(openclaw.__file__)
        diagnostics["openclaw_sys_path_added"] = str(Path(openclaw.__file__).parent / ".openclaw")
    except Exception as e:
        diagnostics["modules"]["openclaw"] = False
        diagnostics["openclaw_error"] = str(e)

    try:
        from agents import brain_minimal
        diagnostics["modules"]["brain_minimal"] = True
        diagnostics["brain_has_get_brain"] = hasattr(brain_minimal, 'get_brain')
    except Exception as e:
        diagnostics["modules"]["brain_minimal"] = False
        diagnostics["brain_minimal_error"] = str(e)

    return diagnostics


if __name__ == "__main__":
    import uvicorn

    try:
        port = int(os.environ.get('PORT', 7860))
        print(f">>> CAIN: Starting uvicorn on port {port}...", flush=True)
        print(f'>>> CAIN: uvicorn.run("app:app", host="0.0.0.0", port={port})', flush=True)
        uvicorn.run("app:app", host="0.0.0.0", port=port, log_config=None)
    except Exception as e:
        print(f"CRITICAL STARTUP ERROR: {e}", flush=True)
        traceback.print_exc()
        sys.exit(1)