Claude Code Claude Code commited on
Commit
0565df0
·
1 Parent(s): 8fdfa0c

Claude Code: isolate startup issue - rename complex app to app_broken.py, create minimal FastAPI test app

Browse files

This isolates whether the RUNNING_APP_STARTING issue is:
- Docker/Infrastructure problem (minimal app will also fail)
- Python code issue (minimal app should succeed)

Co-Authored-By: Claude Code <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +6 -536
  2. app_broken.py +555 -0
app.py CHANGED
@@ -1,553 +1,23 @@
1
  #!/usr/bin/env python3
2
  """
3
- HuggingClaw - Cain Main Entry Point
4
- FastAPI application serving on port 7860.
5
  """
6
  from fastapi import FastAPI
7
- from fastapi.middleware.cors import CORSMiddleware
8
- from fastapi.staticfiles import StaticFiles
9
- from fastapi.responses import FileResponse, JSONResponse
10
- from fastapi import WebSocket
11
- from pydantic import BaseModel
12
- from typing import Optional, Dict, List
13
- from enum import Enum
14
- import os
15
- import sys
16
- from datetime import datetime
17
- import asyncio
18
- from contextlib import asynccontextmanager
19
- import time
20
- import json
21
- import psutil
22
 
23
- # Add /app to sys.path for proper package imports
24
- sys.path.insert(0, "/app")
25
-
26
- # Import system logger
27
- from openclaw.core.system_logger import log_startup, log_heartbeat, get_last_lines
28
- # Import error handlers
29
- from error_handlers import handle_status_file_read, handle_brain_response, handle_websocket_send
30
-
31
- # Track startup time for uptime calculation
32
- START_TIME = time.time()
33
-
34
-
35
- # ============================================================================
36
- # Agent Communication Manager
37
- # ============================================================================
38
-
39
- class AgentRole(str, Enum):
40
- """Agent roles in the HuggingClaw World family."""
41
- ADAM = "adam"
42
- EVE = "eve"
43
- CAIN = "cain"
44
-
45
-
46
- class AgentMessage(BaseModel):
47
- """Message structure for inter-agent communication."""
48
- sender: AgentRole
49
- recipient: AgentRole
50
- content: str
51
- timestamp: str
52
- message_id: Optional[str] = None
53
-
54
-
55
- class AgentState(BaseModel):
56
- """State tracking for each agent including heartbeat."""
57
- agent_id: AgentRole
58
- current_state: str = "idle"
59
- last_heartbeat: float = 0.0
60
- message_queue_size: int = 0
61
- is_active: bool = False
62
-
63
-
64
- class AgentRouter:
65
- """
66
- Centralized message router for inter-agent communication.
67
- Uses asyncio.Queue to prevent state corruption.
68
- """
69
-
70
- def __init__(self):
71
- self._queues: Dict[AgentRole, asyncio.Queue] = {
72
- AgentRole.ADAM: asyncio.Queue(),
73
- AgentRole.EVE: asyncio.Queue(),
74
- AgentRole.CAIN: asyncio.Queue(),
75
- }
76
- self._states: Dict[AgentRole, AgentState] = {
77
- role: AgentState(agent_id=role, last_heartbeat=time.time())
78
- for role in AgentRole
79
- }
80
- self._heartbeat_task: Optional[asyncio.Task] = None
81
-
82
- async def start(self):
83
- """Start the agent router background tasks."""
84
- self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
85
-
86
- async def stop(self):
87
- """Stop the agent router background tasks."""
88
- if self._heartbeat_task:
89
- self._heartbeat_task.cancel()
90
- try:
91
- await self._heartbeat_task
92
- except asyncio.CancelledError:
93
- pass
94
-
95
- async def _heartbeat_loop(self):
96
- """Update heartbeat timestamps every 10 seconds."""
97
- while True:
98
- await asyncio.sleep(10)
99
- now = time.time()
100
- for state in self._states.values():
101
- state.last_heartbeat = now
102
- state.message_queue_size = self._queues[state.agent_id].qsize()
103
- # Mark inactive if no heartbeat for 30 seconds
104
- state.is_active = (now - state.last_heartbeat) < 30
105
-
106
- async def send_message(self, message: AgentMessage) -> bool:
107
- """Send a message to a specific agent's queue."""
108
- recipient = message.recipient
109
- if recipient not in self._queues:
110
- return False
111
- await self._queues[recipient].put(message)
112
- return True
113
-
114
- async def receive_message(self, agent: AgentRole, timeout: float = 1.0) -> Optional[AgentMessage]:
115
- """Receive a message from an agent's queue."""
116
- if agent not in self._queues:
117
- return None
118
- try:
119
- return await asyncio.wait_for(self._queues[agent].get(), timeout=timeout)
120
- except asyncio.TimeoutError:
121
- return None
122
-
123
- def get_all_states(self) -> List[AgentState]:
124
- """Get current state of all agents."""
125
- return list(self._states.values())
126
-
127
- def get_state(self, agent: AgentRole) -> Optional[AgentState]:
128
- """Get state of a specific agent."""
129
- return self._states.get(agent)
130
-
131
- def update_state(self, agent: AgentRole, state: str) -> bool:
132
- """Update the state of a specific agent."""
133
- if agent not in self._states:
134
- return False
135
- self._states[agent].current_state = state
136
- self._states[agent].last_heartbeat = time.time()
137
- return True
138
-
139
-
140
- # Global agent router instance
141
- agent_router = AgentRouter()
142
-
143
-
144
- @asynccontextmanager
145
- async def lifespan(app: FastAPI):
146
- """Lifespan context manager for startup/shutdown events."""
147
- # Startup: log system startup
148
- log_startup("Cain", "1.0.0")
149
-
150
- # Start agent router
151
- await agent_router.start()
152
-
153
- # Start heartbeat task
154
- heartbeat_task = asyncio.create_task(heartbeat_loop())
155
-
156
- yield
157
-
158
- # Shutdown: cancel heartbeat and stop router
159
- heartbeat_task.cancel()
160
- await agent_router.stop()
161
-
162
-
163
- # Background heartbeat loop
164
- async def heartbeat_loop():
165
- """Write heartbeat signal every 30 seconds."""
166
- while True:
167
- await asyncio.sleep(30)
168
- status = get_cain_status().get("current_state", "unknown")
169
- log_heartbeat(status)
170
-
171
-
172
- app = FastAPI(title="HuggingClaw - Cain", version="1.0.0", lifespan=lifespan)
173
-
174
- # Mount static files directory
175
- static_dir = "/app/static"
176
- if os.path.exists(static_dir):
177
- app.mount("/static", StaticFiles(directory=static_dir), name="static")
178
-
179
- # CORS enabled for frontend access
180
- app.add_middleware(
181
- CORSMiddleware,
182
- allow_origins=["*"],
183
- allow_credentials=True,
184
- allow_methods=["*"],
185
- allow_headers=["*"],
186
- )
187
-
188
- class ChatMessage(BaseModel):
189
- message: str
190
-
191
-
192
- def get_cain_status() -> dict:
193
- """Read Cain's current status from cain_status.json."""
194
- return handle_status_file_read()
195
-
196
-
197
- def get_brain_response(message: str) -> str:
198
- """Route message to brain_minimal.py and return response."""
199
- return handle_brain_response(message)
200
 
201
 
202
  @app.get("/")
203
  async def root():
204
  """Root endpoint - simple health check."""
205
- return {"message": "Cain is running"}
206
-
207
-
208
- @app.get("/dashboard", response_class=FileResponse)
209
- async def dashboard():
210
- """Dashboard endpoint serving the UI."""
211
- index_path = f"{static_dir}/index.html"
212
- if os.path.exists(index_path):
213
- return FileResponse(index_path)
214
- # Fallback if dashboard not found
215
- return FileResponse("/app/index.html")
216
-
217
-
218
- @app.get("/status")
219
- async def status():
220
- """Get Cain's health from cain_status.json."""
221
- status_data = get_cain_status()
222
- return {
223
- "agent": "cain",
224
- "health": status_data,
225
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
226
- }
227
-
228
-
229
- # Dashboard API endpoints
230
- @app.get("/api/status")
231
- async def api_status():
232
- """Dashboard API - get status and personality."""
233
- status_data = get_cain_status()
234
- return {
235
- "status": status_data,
236
- "personality": {
237
- "name": "Cain",
238
- "role": "Interaction Agent",
239
- "tone": "friendly",
240
- "response_style": "conversational"
241
- },
242
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
243
- }
244
-
245
-
246
- @app.get("/api/logs")
247
- async def api_logs():
248
- """Dashboard API - get agent logs."""
249
- return {"logs": []}
250
-
251
-
252
- @app.post("/api/chat")
253
- async def api_chat(msg: ChatMessage):
254
- """Dashboard API - chat endpoint."""
255
- response_text = get_brain_response(msg.message)
256
- return {
257
- "agent_response": response_text,
258
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
259
- }
260
-
261
-
262
- @app.post("/chat")
263
- async def chat(msg: ChatMessage):
264
- """Chat endpoint - routes to brain_minimal.py and returns response."""
265
- response_text = get_brain_response(msg.message)
266
- return {
267
- "response": response_text,
268
- "timestamp": datetime.utcnow().isoformat() + "+00:00",
269
- "agent": "cain"
270
- }
271
-
272
-
273
- @app.post("/agents/send")
274
- async def agent_send(msg: AgentMessage):
275
- """Send a message to another agent through the router."""
276
- success = await agent_router.send_message(msg)
277
- return {
278
- "success": success,
279
- "message": "Message queued" if success else "Failed to queue message",
280
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
281
- }
282
-
283
-
284
- @app.get("/agents/{agent_id}/receive")
285
- async def agent_receive(agent_id: str, timeout: float = 1.0):
286
- """Receive a message from the agent's queue."""
287
- try:
288
- role = AgentRole(agent_id)
289
- except ValueError:
290
- return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
291
-
292
- message = await agent_router.receive_message(role, timeout=timeout)
293
- if message:
294
- return {
295
- "message": message.dict(),
296
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
297
- }
298
- return {
299
- "message": None,
300
- "queue_size": agent_router.get_state(role).message_queue_size,
301
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
302
- }
303
-
304
-
305
- @app.post("/agents/{agent_id}/state")
306
- async def agent_update_state(agent_id: str, state: str):
307
- """Update the state of an agent."""
308
- try:
309
- role = AgentRole(agent_id)
310
- except ValueError:
311
- return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
312
-
313
- success = agent_router.update_state(role, state)
314
- return {
315
- "success": success,
316
- "agent_id": agent_id,
317
- "new_state": state if success else None,
318
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
319
- }
320
-
321
-
322
- @app.websocket("/ws")
323
- async def websocket_endpoint(websocket: WebSocket):
324
- """WebSocket endpoint for real-time dashboard updates."""
325
- await websocket.accept()
326
- try:
327
- while True:
328
- # Send heartbeat every 5 seconds using error handler
329
- status_data = get_cain_status()
330
- if not await handle_websocket_send(websocket, status_data):
331
- break
332
- await asyncio.sleep(5)
333
- except Exception as e:
334
- pass
335
- finally:
336
- await websocket.close()
337
-
338
-
339
- @app.get("/admin/system")
340
- async def admin_system():
341
- """Admin endpoint - read last 50 lines of system log."""
342
- lines = get_last_lines(50)
343
- return {
344
- "log_file": "logs/system.log",
345
- "line_count": len(lines),
346
- "lines": lines,
347
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
348
- }
349
 
350
 
351
  @app.get("/health")
352
  async def health():
353
- """
354
- Health check endpoint with proper HTTP status codes.
355
- Returns 200 if healthy, 503 if unhealthy with descriptive error.
356
-
357
- Checks:
358
- 1. Brain import status
359
- 2. Data persistence files (JSON-based storage)
360
- 3. Frontend asset availability (static/index.html)
361
- """
362
- from fastapi import status as http_status
363
-
364
- checks = {}
365
- is_healthy = True
366
- error_message = None
367
-
368
- # 1. Check brain_minimal can be imported
369
- brain_ok = False
370
- try:
371
- from openclaw.agents.brain_minimal import BrainMinimal
372
- brain = BrainMinimal(agent_name="cain", legacy_mode=True)
373
- brain_ok = True
374
- checks["brain_import"] = "ok"
375
- except ImportError as e:
376
- checks["brain_import"] = f"failed: ImportError: {str(e)}"
377
- is_healthy = False
378
- error_message = f"Brain import failed: {str(e)}"
379
- except Exception as e:
380
- checks["brain_import"] = f"failed: {type(e).__name__}: {str(e)}"
381
- is_healthy = False
382
- error_message = f"Brain import failed: {type(e).__name__}: {str(e)}"
383
-
384
- # 2. Check data persistence files (JSON-based)
385
- persistence_ok = True
386
- cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
387
- registry_path = "/app/openclaw/.openclaw/agents/registry.json"
388
-
389
- # Check cain_status.json
390
- try:
391
- with open(cain_status_path, "r") as f:
392
- status_data = json.load(f)
393
- current_state = status_data.get("current_state", "unknown")
394
- checks["persistence_cain_status"] = "ok"
395
- if current_state == "error":
396
- checks["persistence_cain_status"] = "ok (error state)"
397
- except FileNotFoundError:
398
- checks["persistence_cain_status"] = "failed: file not found"
399
- persistence_ok = False
400
- except json.JSONDecodeError as e:
401
- checks["persistence_cain_status"] = f"failed: invalid JSON: {str(e)}"
402
- persistence_ok = False
403
- except Exception as e:
404
- checks["persistence_cain_status"] = f"failed: {type(e).__name__}: {str(e)}"
405
- persistence_ok = False
406
-
407
- # Check registry.json
408
- try:
409
- with open(registry_path, "r") as f:
410
- json.load(f)
411
- checks["persistence_registry"] = "ok"
412
- except FileNotFoundError:
413
- checks["persistence_registry"] = "failed: file not found"
414
- persistence_ok = False
415
- except json.JSONDecodeError as e:
416
- checks["persistence_registry"] = f"failed: invalid JSON: {str(e)}"
417
- persistence_ok = False
418
- except Exception as e:
419
- checks["persistence_registry"] = f"failed: {type(e).__name__}: {str(e)}"
420
- persistence_ok = False
421
-
422
- checks["persistence"] = "ok" if persistence_ok else "degraded"
423
-
424
- # 3. Check frontend asset availability
425
- frontend_ok = False
426
- index_path = f"{static_dir}/index.html"
427
- fallback_path = "/app/index.html"
428
-
429
- if os.path.exists(index_path):
430
- checks["frontend_assets"] = "ok"
431
- frontend_ok = True
432
- elif os.path.exists(fallback_path):
433
- checks["frontend_assets"] = "ok (fallback)"
434
- frontend_ok = True
435
- else:
436
- checks["frontend_assets"] = "failed: index.html not found"
437
- # Frontend missing is degraded, not critical failure
438
-
439
- # 4. Check agent router heartbeat
440
- now = time.time()
441
- cain_state = agent_router.get_state(AgentRole.CAIN)
442
- heartbeat_age = now - cain_state.last_heartbeat
443
- router_ok = heartbeat_age < 30
444
- checks["agent_router"] = "ok" if router_ok else f"degraded: stale ({heartbeat_age:.1f}s)"
445
-
446
- # Determine overall status
447
- # Critical failure: brain not available
448
- if not brain_ok:
449
- return JSONResponse(
450
- status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
451
- content={
452
- "status": "critical",
453
- "brain": "unavailable",
454
- "persistence": "ok" if persistence_ok else "error",
455
- "frontend": "available" if frontend_ok else "unavailable",
456
- "checks": checks,
457
- "error": error_message,
458
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
459
- }
460
- )
461
-
462
- # Healthy response
463
- return {
464
- "status": "healthy" if is_healthy else "degraded",
465
- "brain": "available",
466
- "persistence": "ok" if persistence_ok else "error",
467
- "frontend": "available" if frontend_ok else "unavailable",
468
- "agent_router": "ok" if router_ok else "stale",
469
- "uptime_seconds": round(time.time() - START_TIME, 2),
470
- "checks": checks,
471
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
472
- }
473
-
474
-
475
- @app.get("/debug/health")
476
- async def debug_health():
477
- """Debug health endpoint with detailed system status."""
478
- # 1. Uptime in seconds
479
- uptime = time.time() - START_TIME
480
-
481
- # 2. Current memory usage of the current process (brain_minimal runs in same process)
482
- process = psutil.Process(os.getpid())
483
- memory_info = process.memory_info()
484
- memory_mb = memory_info.rss / 1024 / 1024 # Convert to MB
485
-
486
- # 3. Last 5 lines from cain_status.json (read as structured JSON)
487
- cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
488
- cain_status_lines = []
489
- try:
490
- if os.path.exists(cain_status_path):
491
- with open(cain_status_path, "r") as f:
492
- content = f.read()
493
- lines = content.strip().split("\n")
494
- cain_status_lines = lines[-5:] if len(lines) > 5 else lines
495
- else:
496
- cain_status_lines = ["cain_status.json not found"]
497
- except Exception as e:
498
- cain_status_lines = [f"Error reading cain_status.json: {str(e)}"]
499
-
500
- # 4. List of loaded Python modules
501
- loaded_modules = sorted([name for name in sys.modules.keys() if not name.startswith("_")])[:100]
502
-
503
- return {
504
- "uptime_seconds": round(uptime, 2),
505
- "memory": {
506
- "rss_mb": round(memory_mb, 2),
507
- "vms_mb": round(memory_info.vms / 1024 / 1024, 2)
508
- },
509
- "cain_status_last_lines": cain_status_lines,
510
- "loaded_modules_count": len(loaded_modules),
511
- "loaded_modules": loaded_modules,
512
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
513
- }
514
-
515
-
516
- @app.get("/health/agents")
517
- async def health_agents():
518
- """Health check endpoint returning status of all agents."""
519
- agents_status = []
520
- now = time.time()
521
-
522
- for state in agent_router.get_all_states():
523
- # Calculate time since last heartbeat
524
- heartbeat_age = now - state.last_heartbeat
525
-
526
- # Determine health status
527
- if heartbeat_age < 15:
528
- health = "healthy"
529
- elif heartbeat_age < 30:
530
- health = "degraded"
531
- else:
532
- health = "unhealthy"
533
-
534
- agents_status.append({
535
- "agent_id": state.agent_id,
536
- "current_state": state.current_state,
537
- "is_active": state.is_active,
538
- "health": health,
539
- "last_heartbeat": state.last_heartbeat,
540
- "heartbeat_age_seconds": round(heartbeat_age, 2),
541
- "message_queue_size": state.message_queue_size,
542
- "last_heartbeat_iso": datetime.fromtimestamp(state.last_heartbeat).isoformat() + "+00:00"
543
- })
544
-
545
- return {
546
- "agents": agents_status,
547
- "total_agents": len(agents_status),
548
- "active_agents": sum(1 for a in agents_status if a["is_active"]),
549
- "timestamp": datetime.utcnow().isoformat() + "+00:00"
550
- }
551
 
552
 
553
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
  """
3
+ Minimal HuggingClaw - Cain
4
+ Testing basic FastAPI container functionality.
5
  """
6
  from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ app = FastAPI(title="HuggingClaw - Cain (Minimal Test)", version="0.0.1")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  @app.get("/")
12
  async def root():
13
  """Root endpoint - simple health check."""
14
+ return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  @app.get("/health")
18
  async def health():
19
+ """Health check endpoint."""
20
+ return {"status": "healthy"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  if __name__ == "__main__":
app_broken.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HuggingClaw - Cain Main Entry Point
4
+ FastAPI application serving on port 7860.
5
+ """
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.responses import FileResponse, JSONResponse
10
+ from fastapi import WebSocket
11
+ from pydantic import BaseModel
12
+ from typing import Optional, Dict, List
13
+ from enum import Enum
14
+ import os
15
+ import sys
16
+ from datetime import datetime
17
+ import asyncio
18
+ from contextlib import asynccontextmanager
19
+ import time
20
+ import json
21
+ import psutil
22
+
23
+ # Add /app to sys.path for proper package imports
24
+ sys.path.insert(0, "/app")
25
+
26
+ # Import system logger
27
+ from openclaw.core.system_logger import log_startup, log_heartbeat, get_last_lines
28
+ # Import error handlers
29
+ from error_handlers import handle_status_file_read, handle_brain_response, handle_websocket_send
30
+
31
+ # Track startup time for uptime calculation
32
+ START_TIME = time.time()
33
+
34
+
35
+ # ============================================================================
36
+ # Agent Communication Manager
37
+ # ============================================================================
38
+
39
+ class AgentRole(str, Enum):
40
+ """Agent roles in the HuggingClaw World family."""
41
+ ADAM = "adam"
42
+ EVE = "eve"
43
+ CAIN = "cain"
44
+
45
+
46
+ class AgentMessage(BaseModel):
47
+ """Message structure for inter-agent communication."""
48
+ sender: AgentRole
49
+ recipient: AgentRole
50
+ content: str
51
+ timestamp: str
52
+ message_id: Optional[str] = None
53
+
54
+
55
+ class AgentState(BaseModel):
56
+ """State tracking for each agent including heartbeat."""
57
+ agent_id: AgentRole
58
+ current_state: str = "idle"
59
+ last_heartbeat: float = 0.0
60
+ message_queue_size: int = 0
61
+ is_active: bool = False
62
+
63
+
64
+ class AgentRouter:
65
+ """
66
+ Centralized message router for inter-agent communication.
67
+ Uses asyncio.Queue to prevent state corruption.
68
+ """
69
+
70
+ def __init__(self):
71
+ self._queues: Dict[AgentRole, asyncio.Queue] = {
72
+ AgentRole.ADAM: asyncio.Queue(),
73
+ AgentRole.EVE: asyncio.Queue(),
74
+ AgentRole.CAIN: asyncio.Queue(),
75
+ }
76
+ self._states: Dict[AgentRole, AgentState] = {
77
+ role: AgentState(agent_id=role, last_heartbeat=time.time())
78
+ for role in AgentRole
79
+ }
80
+ self._heartbeat_task: Optional[asyncio.Task] = None
81
+
82
+ async def start(self):
83
+ """Start the agent router background tasks."""
84
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
85
+
86
+ async def stop(self):
87
+ """Stop the agent router background tasks."""
88
+ if self._heartbeat_task:
89
+ self._heartbeat_task.cancel()
90
+ try:
91
+ await self._heartbeat_task
92
+ except asyncio.CancelledError:
93
+ pass
94
+
95
+ async def _heartbeat_loop(self):
96
+ """Update heartbeat timestamps every 10 seconds."""
97
+ while True:
98
+ await asyncio.sleep(10)
99
+ now = time.time()
100
+ for state in self._states.values():
101
+ state.last_heartbeat = now
102
+ state.message_queue_size = self._queues[state.agent_id].qsize()
103
+ # Mark inactive if no heartbeat for 30 seconds
104
+ state.is_active = (now - state.last_heartbeat) < 30
105
+
106
+ async def send_message(self, message: AgentMessage) -> bool:
107
+ """Send a message to a specific agent's queue."""
108
+ recipient = message.recipient
109
+ if recipient not in self._queues:
110
+ return False
111
+ await self._queues[recipient].put(message)
112
+ return True
113
+
114
+ async def receive_message(self, agent: AgentRole, timeout: float = 1.0) -> Optional[AgentMessage]:
115
+ """Receive a message from an agent's queue."""
116
+ if agent not in self._queues:
117
+ return None
118
+ try:
119
+ return await asyncio.wait_for(self._queues[agent].get(), timeout=timeout)
120
+ except asyncio.TimeoutError:
121
+ return None
122
+
123
+ def get_all_states(self) -> List[AgentState]:
124
+ """Get current state of all agents."""
125
+ return list(self._states.values())
126
+
127
+ def get_state(self, agent: AgentRole) -> Optional[AgentState]:
128
+ """Get state of a specific agent."""
129
+ return self._states.get(agent)
130
+
131
+ def update_state(self, agent: AgentRole, state: str) -> bool:
132
+ """Update the state of a specific agent."""
133
+ if agent not in self._states:
134
+ return False
135
+ self._states[agent].current_state = state
136
+ self._states[agent].last_heartbeat = time.time()
137
+ return True
138
+
139
+
140
+ # Global agent router instance
141
+ agent_router = AgentRouter()
142
+
143
+
144
+ @asynccontextmanager
145
+ async def lifespan(app: FastAPI):
146
+ """Lifespan context manager for startup/shutdown events."""
147
+ # Startup: log system startup
148
+ log_startup("Cain", "1.0.0")
149
+
150
+ # Start agent router
151
+ await agent_router.start()
152
+
153
+ # Start heartbeat task
154
+ heartbeat_task = asyncio.create_task(heartbeat_loop())
155
+
156
+ yield
157
+
158
+ # Shutdown: cancel heartbeat and stop router
159
+ heartbeat_task.cancel()
160
+ await agent_router.stop()
161
+
162
+
163
+ # Background heartbeat loop
164
+ async def heartbeat_loop():
165
+ """Write heartbeat signal every 30 seconds."""
166
+ while True:
167
+ await asyncio.sleep(30)
168
+ status = get_cain_status().get("current_state", "unknown")
169
+ log_heartbeat(status)
170
+
171
+
172
+ app = FastAPI(title="HuggingClaw - Cain", version="1.0.0", lifespan=lifespan)
173
+
174
+ # Mount static files directory
175
+ static_dir = "/app/static"
176
+ if os.path.exists(static_dir):
177
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
178
+
179
+ # CORS enabled for frontend access
180
+ app.add_middleware(
181
+ CORSMiddleware,
182
+ allow_origins=["*"],
183
+ allow_credentials=True,
184
+ allow_methods=["*"],
185
+ allow_headers=["*"],
186
+ )
187
+
188
+ class ChatMessage(BaseModel):
189
+ message: str
190
+
191
+
192
+ def get_cain_status() -> dict:
193
+ """Read Cain's current status from cain_status.json."""
194
+ return handle_status_file_read()
195
+
196
+
197
+ def get_brain_response(message: str) -> str:
198
+ """Route message to brain_minimal.py and return response."""
199
+ return handle_brain_response(message)
200
+
201
+
202
+ @app.get("/")
203
+ async def root():
204
+ """Root endpoint - simple health check."""
205
+ return {"message": "Cain is running"}
206
+
207
+
208
+ @app.get("/dashboard", response_class=FileResponse)
209
+ async def dashboard():
210
+ """Dashboard endpoint serving the UI."""
211
+ index_path = f"{static_dir}/index.html"
212
+ if os.path.exists(index_path):
213
+ return FileResponse(index_path)
214
+ # Fallback if dashboard not found
215
+ return FileResponse("/app/index.html")
216
+
217
+
218
+ @app.get("/status")
219
+ async def status():
220
+ """Get Cain's health from cain_status.json."""
221
+ status_data = get_cain_status()
222
+ return {
223
+ "agent": "cain",
224
+ "health": status_data,
225
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
226
+ }
227
+
228
+
229
+ # Dashboard API endpoints
230
+ @app.get("/api/status")
231
+ async def api_status():
232
+ """Dashboard API - get status and personality."""
233
+ status_data = get_cain_status()
234
+ return {
235
+ "status": status_data,
236
+ "personality": {
237
+ "name": "Cain",
238
+ "role": "Interaction Agent",
239
+ "tone": "friendly",
240
+ "response_style": "conversational"
241
+ },
242
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
243
+ }
244
+
245
+
246
+ @app.get("/api/logs")
247
+ async def api_logs():
248
+ """Dashboard API - get agent logs."""
249
+ return {"logs": []}
250
+
251
+
252
+ @app.post("/api/chat")
253
+ async def api_chat(msg: ChatMessage):
254
+ """Dashboard API - chat endpoint."""
255
+ response_text = get_brain_response(msg.message)
256
+ return {
257
+ "agent_response": response_text,
258
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
259
+ }
260
+
261
+
262
+ @app.post("/chat")
263
+ async def chat(msg: ChatMessage):
264
+ """Chat endpoint - routes to brain_minimal.py and returns response."""
265
+ response_text = get_brain_response(msg.message)
266
+ return {
267
+ "response": response_text,
268
+ "timestamp": datetime.utcnow().isoformat() + "+00:00",
269
+ "agent": "cain"
270
+ }
271
+
272
+
273
+ @app.post("/agents/send")
274
+ async def agent_send(msg: AgentMessage):
275
+ """Send a message to another agent through the router."""
276
+ success = await agent_router.send_message(msg)
277
+ return {
278
+ "success": success,
279
+ "message": "Message queued" if success else "Failed to queue message",
280
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
281
+ }
282
+
283
+
284
+ @app.get("/agents/{agent_id}/receive")
285
+ async def agent_receive(agent_id: str, timeout: float = 1.0):
286
+ """Receive a message from the agent's queue."""
287
+ try:
288
+ role = AgentRole(agent_id)
289
+ except ValueError:
290
+ return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
291
+
292
+ message = await agent_router.receive_message(role, timeout=timeout)
293
+ if message:
294
+ return {
295
+ "message": message.dict(),
296
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
297
+ }
298
+ return {
299
+ "message": None,
300
+ "queue_size": agent_router.get_state(role).message_queue_size,
301
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
302
+ }
303
+
304
+
305
+ @app.post("/agents/{agent_id}/state")
306
+ async def agent_update_state(agent_id: str, state: str):
307
+ """Update the state of an agent."""
308
+ try:
309
+ role = AgentRole(agent_id)
310
+ except ValueError:
311
+ return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
312
+
313
+ success = agent_router.update_state(role, state)
314
+ return {
315
+ "success": success,
316
+ "agent_id": agent_id,
317
+ "new_state": state if success else None,
318
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
319
+ }
320
+
321
+
322
+ @app.websocket("/ws")
323
+ async def websocket_endpoint(websocket: WebSocket):
324
+ """WebSocket endpoint for real-time dashboard updates."""
325
+ await websocket.accept()
326
+ try:
327
+ while True:
328
+ # Send heartbeat every 5 seconds using error handler
329
+ status_data = get_cain_status()
330
+ if not await handle_websocket_send(websocket, status_data):
331
+ break
332
+ await asyncio.sleep(5)
333
+ except Exception as e:
334
+ pass
335
+ finally:
336
+ await websocket.close()
337
+
338
+
339
+ @app.get("/admin/system")
340
+ async def admin_system():
341
+ """Admin endpoint - read last 50 lines of system log."""
342
+ lines = get_last_lines(50)
343
+ return {
344
+ "log_file": "logs/system.log",
345
+ "line_count": len(lines),
346
+ "lines": lines,
347
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
348
+ }
349
+
350
+
351
+ @app.get("/health")
352
+ async def health():
353
+ """
354
+ Health check endpoint with proper HTTP status codes.
355
+ Returns 200 if healthy, 503 if unhealthy with descriptive error.
356
+
357
+ Checks:
358
+ 1. Brain import status
359
+ 2. Data persistence files (JSON-based storage)
360
+ 3. Frontend asset availability (static/index.html)
361
+ """
362
+ from fastapi import status as http_status
363
+
364
+ checks = {}
365
+ is_healthy = True
366
+ error_message = None
367
+
368
+ # 1. Check brain_minimal can be imported
369
+ brain_ok = False
370
+ try:
371
+ from openclaw.agents.brain_minimal import BrainMinimal
372
+ brain = BrainMinimal(agent_name="cain", legacy_mode=True)
373
+ brain_ok = True
374
+ checks["brain_import"] = "ok"
375
+ except ImportError as e:
376
+ checks["brain_import"] = f"failed: ImportError: {str(e)}"
377
+ is_healthy = False
378
+ error_message = f"Brain import failed: {str(e)}"
379
+ except Exception as e:
380
+ checks["brain_import"] = f"failed: {type(e).__name__}: {str(e)}"
381
+ is_healthy = False
382
+ error_message = f"Brain import failed: {type(e).__name__}: {str(e)}"
383
+
384
+ # 2. Check data persistence files (JSON-based)
385
+ persistence_ok = True
386
+ cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
387
+ registry_path = "/app/openclaw/.openclaw/agents/registry.json"
388
+
389
+ # Check cain_status.json
390
+ try:
391
+ with open(cain_status_path, "r") as f:
392
+ status_data = json.load(f)
393
+ current_state = status_data.get("current_state", "unknown")
394
+ checks["persistence_cain_status"] = "ok"
395
+ if current_state == "error":
396
+ checks["persistence_cain_status"] = "ok (error state)"
397
+ except FileNotFoundError:
398
+ checks["persistence_cain_status"] = "failed: file not found"
399
+ persistence_ok = False
400
+ except json.JSONDecodeError as e:
401
+ checks["persistence_cain_status"] = f"failed: invalid JSON: {str(e)}"
402
+ persistence_ok = False
403
+ except Exception as e:
404
+ checks["persistence_cain_status"] = f"failed: {type(e).__name__}: {str(e)}"
405
+ persistence_ok = False
406
+
407
+ # Check registry.json
408
+ try:
409
+ with open(registry_path, "r") as f:
410
+ json.load(f)
411
+ checks["persistence_registry"] = "ok"
412
+ except FileNotFoundError:
413
+ checks["persistence_registry"] = "failed: file not found"
414
+ persistence_ok = False
415
+ except json.JSONDecodeError as e:
416
+ checks["persistence_registry"] = f"failed: invalid JSON: {str(e)}"
417
+ persistence_ok = False
418
+ except Exception as e:
419
+ checks["persistence_registry"] = f"failed: {type(e).__name__}: {str(e)}"
420
+ persistence_ok = False
421
+
422
+ checks["persistence"] = "ok" if persistence_ok else "degraded"
423
+
424
+ # 3. Check frontend asset availability
425
+ frontend_ok = False
426
+ index_path = f"{static_dir}/index.html"
427
+ fallback_path = "/app/index.html"
428
+
429
+ if os.path.exists(index_path):
430
+ checks["frontend_assets"] = "ok"
431
+ frontend_ok = True
432
+ elif os.path.exists(fallback_path):
433
+ checks["frontend_assets"] = "ok (fallback)"
434
+ frontend_ok = True
435
+ else:
436
+ checks["frontend_assets"] = "failed: index.html not found"
437
+ # Frontend missing is degraded, not critical failure
438
+
439
+ # 4. Check agent router heartbeat
440
+ now = time.time()
441
+ cain_state = agent_router.get_state(AgentRole.CAIN)
442
+ heartbeat_age = now - cain_state.last_heartbeat
443
+ router_ok = heartbeat_age < 30
444
+ checks["agent_router"] = "ok" if router_ok else f"degraded: stale ({heartbeat_age:.1f}s)"
445
+
446
+ # Determine overall status
447
+ # Critical failure: brain not available
448
+ if not brain_ok:
449
+ return JSONResponse(
450
+ status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
451
+ content={
452
+ "status": "critical",
453
+ "brain": "unavailable",
454
+ "persistence": "ok" if persistence_ok else "error",
455
+ "frontend": "available" if frontend_ok else "unavailable",
456
+ "checks": checks,
457
+ "error": error_message,
458
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
459
+ }
460
+ )
461
+
462
+ # Healthy response
463
+ return {
464
+ "status": "healthy" if is_healthy else "degraded",
465
+ "brain": "available",
466
+ "persistence": "ok" if persistence_ok else "error",
467
+ "frontend": "available" if frontend_ok else "unavailable",
468
+ "agent_router": "ok" if router_ok else "stale",
469
+ "uptime_seconds": round(time.time() - START_TIME, 2),
470
+ "checks": checks,
471
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
472
+ }
473
+
474
+
475
+ @app.get("/debug/health")
476
+ async def debug_health():
477
+ """Debug health endpoint with detailed system status."""
478
+ # 1. Uptime in seconds
479
+ uptime = time.time() - START_TIME
480
+
481
+ # 2. Current memory usage of the current process (brain_minimal runs in same process)
482
+ process = psutil.Process(os.getpid())
483
+ memory_info = process.memory_info()
484
+ memory_mb = memory_info.rss / 1024 / 1024 # Convert to MB
485
+
486
+ # 3. Last 5 lines from cain_status.json (read as structured JSON)
487
+ cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
488
+ cain_status_lines = []
489
+ try:
490
+ if os.path.exists(cain_status_path):
491
+ with open(cain_status_path, "r") as f:
492
+ content = f.read()
493
+ lines = content.strip().split("\n")
494
+ cain_status_lines = lines[-5:] if len(lines) > 5 else lines
495
+ else:
496
+ cain_status_lines = ["cain_status.json not found"]
497
+ except Exception as e:
498
+ cain_status_lines = [f"Error reading cain_status.json: {str(e)}"]
499
+
500
+ # 4. List of loaded Python modules
501
+ loaded_modules = sorted([name for name in sys.modules.keys() if not name.startswith("_")])[:100]
502
+
503
+ return {
504
+ "uptime_seconds": round(uptime, 2),
505
+ "memory": {
506
+ "rss_mb": round(memory_mb, 2),
507
+ "vms_mb": round(memory_info.vms / 1024 / 1024, 2)
508
+ },
509
+ "cain_status_last_lines": cain_status_lines,
510
+ "loaded_modules_count": len(loaded_modules),
511
+ "loaded_modules": loaded_modules,
512
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
513
+ }
514
+
515
+
516
+ @app.get("/health/agents")
517
+ async def health_agents():
518
+ """Health check endpoint returning status of all agents."""
519
+ agents_status = []
520
+ now = time.time()
521
+
522
+ for state in agent_router.get_all_states():
523
+ # Calculate time since last heartbeat
524
+ heartbeat_age = now - state.last_heartbeat
525
+
526
+ # Determine health status
527
+ if heartbeat_age < 15:
528
+ health = "healthy"
529
+ elif heartbeat_age < 30:
530
+ health = "degraded"
531
+ else:
532
+ health = "unhealthy"
533
+
534
+ agents_status.append({
535
+ "agent_id": state.agent_id,
536
+ "current_state": state.current_state,
537
+ "is_active": state.is_active,
538
+ "health": health,
539
+ "last_heartbeat": state.last_heartbeat,
540
+ "heartbeat_age_seconds": round(heartbeat_age, 2),
541
+ "message_queue_size": state.message_queue_size,
542
+ "last_heartbeat_iso": datetime.fromtimestamp(state.last_heartbeat).isoformat() + "+00:00"
543
+ })
544
+
545
+ return {
546
+ "agents": agents_status,
547
+ "total_agents": len(agents_status),
548
+ "active_agents": sum(1 for a in agents_status if a["is_active"]),
549
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
550
+ }
551
+
552
+
553
+ if __name__ == "__main__":
554
+ import uvicorn
555
+ uvicorn.run(app, host="0.0.0.0", port=7860)