Spaces:
Runtime error
Runtime error
feat: add agent memory + God silent mode
Browse files- Adam & Eve memory: leverages OpenClaw's ~/.openclaw/workspace/memory/ system
- Persistent markdown memory file (family-conversation.md)
- Agents can save learnings via [MEMORY: ...] tags
- Auto-backed up by openclaw_persist.py to HF Dataset
- Loaded into system prompt each turn
- God chatlog: only posts when making actual changes
- No more "starting review" announcements
- Uses [PROBLEM] and [FIX] markers in output
- Silent when no issues found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/conversation-loop.py +279 -138
scripts/conversation-loop.py
CHANGED
|
@@ -42,6 +42,7 @@ then delegate ALL coding work to Claude Code CLI.
|
|
| 42 |
"""
|
| 43 |
import json, time, re, requests, sys, os, io, subprocess, threading, datetime
|
| 44 |
from collections import deque
|
|
|
|
| 45 |
|
| 46 |
# Force unbuffered output
|
| 47 |
sys.stdout.reconfigure(line_buffering=True)
|
|
@@ -383,42 +384,136 @@ CLAUDE_WORK_DIR = "/tmp/claude-workspace"
|
|
| 383 |
CLAUDE_TIMEOUT = 300 # 5 minutes
|
| 384 |
TURN_INTERVAL = 15 # seconds between turns — fast enough for lively discussion
|
| 385 |
|
| 386 |
-
def action_claude_code(task):
|
| 387 |
-
"""Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
|
| 388 |
-
if not child_state["created"]:
|
| 389 |
-
return f"{CHILD_NAME} not born yet."
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
try:
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
try:
|
| 398 |
subprocess.run(
|
| 399 |
"git fetch origin && git reset --hard origin/main",
|
| 400 |
-
shell=True, cwd=
|
| 401 |
capture_output=True, check=True
|
| 402 |
)
|
| 403 |
except Exception:
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
subprocess.run(
|
| 406 |
-
f"git clone --depth 20 {repo_url} {
|
| 407 |
shell=True, timeout=60, capture_output=True, check=True
|
| 408 |
)
|
|
|
|
|
|
|
| 409 |
else:
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
subprocess.run(
|
| 413 |
-
f"git clone --depth 20 {repo_url} {
|
| 414 |
shell=True, timeout=60, capture_output=True, check=True
|
| 415 |
)
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
subprocess.run('git config user.
|
| 419 |
-
shell=True, cwd=
|
|
|
|
|
|
|
|
|
|
| 420 |
except Exception as e:
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
# 2. Run Claude Code with z.ai backend (Zhipu GLM)
|
| 424 |
env = os.environ.copy()
|
|
@@ -621,15 +716,11 @@ def format_context(ctx):
|
|
| 621 |
|
| 622 |
|
| 623 |
def enrich_task_with_context(task_desc, ctx):
|
| 624 |
-
"""Append
|
| 625 |
parts = [task_desc]
|
| 626 |
-
|
| 627 |
-
parts.append(f"\
|
| 628 |
-
parts.append(f"Space ID: {CHILD_SPACE_ID}")
|
| 629 |
-
parts.append(f"Dataset ID: {CHILD_DATASET_ID}")
|
| 630 |
-
parts.append(f"Current stage: {child_state['stage']}")
|
| 631 |
parts.append(f"Health: {ctx.get('health', 'unknown')}")
|
| 632 |
-
parts.append(f"Environment: {ctx.get('env', 'unknown')}")
|
| 633 |
return "\n".join(parts)
|
| 634 |
|
| 635 |
|
|
@@ -798,6 +889,106 @@ def set_bubble(url, text_en, text_zh=""):
|
|
| 798 |
pass
|
| 799 |
|
| 800 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 802 |
# MODULE 5: TURN EXECUTION — Parse [TASK] and route to Claude Code
|
| 803 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -986,9 +1177,13 @@ def parse_and_execute_turn(raw_text, ctx):
|
|
| 986 |
if _discussion_loop_count >= 2:
|
| 987 |
print(f"[LOOP-DISCUSS] WARNING: {_discussion_loop_count} consecutive discussion-only turns with CC IDLE and child alive!")
|
| 988 |
|
|
|
|
|
|
|
|
|
|
| 989 |
# Clean text for display
|
| 990 |
clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
|
| 991 |
-
clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean)
|
|
|
|
| 992 |
|
| 993 |
return clean, results, task_assigned
|
| 994 |
|
|
@@ -1101,6 +1296,12 @@ AVAILABLE ACTIONS:
|
|
| 1101 |
[ACTION: create_child] — Create {CHILD_NAME} (if not born)
|
| 1102 |
[ACTION: terminate_cc] — Terminate a STUCK Claude Code process (use when CC has no new output for 180s+)
|
| 1103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
HF SPACES TECHNICAL NOTES:
|
| 1105 |
- We use sdk: docker (NOT gradio). All Spaces run via Dockerfile.
|
| 1106 |
- Docker containers MUST bind port 7860.
|
|
@@ -1175,6 +1376,11 @@ def build_user_prompt(speaker, other, ctx):
|
|
| 1175 |
parts.append(f"YOU MUST write a [TASK]...[/TASK] block NOW. Do NOT write another discussion response.")
|
| 1176 |
parts.append(f"Examples of tasks: 'Check the logs', 'Read config.py', 'Add a feature', 'Fix a bug', etc.")
|
| 1177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
|
| 1179 |
parts.append("English first, then --- separator, then Chinese translation.")
|
| 1180 |
|
|
@@ -1370,29 +1576,11 @@ def do_god_turn():
|
|
| 1370 |
"""
|
| 1371 |
global last_action_results
|
| 1372 |
|
| 1373 |
-
# 1. Clone/update Home Space repo
|
| 1374 |
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{HOME_SPACE_ID}"
|
| 1375 |
-
|
| 1376 |
-
if os.path.exists(f"{GOD_WORK_DIR}/.git"):
|
| 1377 |
-
subprocess.run(
|
| 1378 |
-
"git fetch origin && git reset --hard origin/main",
|
| 1379 |
-
shell=True, cwd=GOD_WORK_DIR, timeout=30,
|
| 1380 |
-
capture_output=True, check=True
|
| 1381 |
-
)
|
| 1382 |
-
else:
|
| 1383 |
-
if os.path.exists(GOD_WORK_DIR):
|
| 1384 |
-
subprocess.run(f"rm -rf {GOD_WORK_DIR}", shell=True, capture_output=True)
|
| 1385 |
-
subprocess.run(
|
| 1386 |
-
f"git clone --depth 20 {repo_url} {GOD_WORK_DIR}",
|
| 1387 |
-
shell=True, timeout=60, capture_output=True, check=True
|
| 1388 |
-
)
|
| 1389 |
-
subprocess.run('git config user.name "God (Claude Code)"',
|
| 1390 |
-
shell=True, cwd=GOD_WORK_DIR, capture_output=True)
|
| 1391 |
-
subprocess.run('git config user.email "god@huggingclaw"',
|
| 1392 |
-
shell=True, cwd=GOD_WORK_DIR, capture_output=True)
|
| 1393 |
-
except Exception as e:
|
| 1394 |
-
print(f"[God] Failed to prepare workspace: {e}")
|
| 1395 |
return
|
|
|
|
| 1396 |
|
| 1397 |
# Record HEAD before Claude Code runs (to detect if God pushed changes)
|
| 1398 |
try:
|
|
@@ -1411,45 +1599,18 @@ def do_god_turn():
|
|
| 1411 |
except Exception as e:
|
| 1412 |
print(f"[God] Warning: Could not write context file: {e}")
|
| 1413 |
|
| 1414 |
-
# 3. Build God's prompt
|
| 1415 |
-
prompt = f"""
|
| 1416 |
-
You have the SAME capabilities as a human operator running Claude Code locally.
|
| 1417 |
-
|
| 1418 |
-
## Current System State
|
| 1419 |
{context}
|
| 1420 |
|
| 1421 |
-
##
|
| 1422 |
-
1.
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
- Pre-seed action history so agents know what is already done
|
| 1430 |
-
- Improve rate limit handling
|
| 1431 |
-
- Add better loop detection or guardrails
|
| 1432 |
-
4. DEPLOY: If you made changes, commit and push:
|
| 1433 |
-
git add scripts/conversation-loop.py
|
| 1434 |
-
git commit -m "god: <brief description>"
|
| 1435 |
-
git push
|
| 1436 |
-
WARNING: Pushing restarts the Space. Only push if the fix is correct and necessary.
|
| 1437 |
-
5. REPORT: At the very end of your output, write a single line starting with [REPORT] that summarizes
|
| 1438 |
-
what you found and what you did. This line will be shown to Adam & Eve in the chatlog.
|
| 1439 |
-
Examples:
|
| 1440 |
-
- [REPORT] System is healthy. Adam & Eve are making good progress on Cain's infrastructure.
|
| 1441 |
-
- [REPORT] Found agents stuck in env var discussion loop. Fixed system prompt to inject completed action history.
|
| 1442 |
-
- [REPORT] Rate limit active, no conversation happening. No mechanism issues found.
|
| 1443 |
-
Be specific about what you observed and what you changed (if anything).
|
| 1444 |
-
|
| 1445 |
-
## Rules
|
| 1446 |
-
- Do NOT modify Cain's Space or code — only improve conversation-loop.py (the mechanism).
|
| 1447 |
-
- Do NOT push trivial or cosmetic changes — only fix real problems.
|
| 1448 |
-
- If everything looks healthy, just report "all clear" and exit quickly.
|
| 1449 |
-
- Be conservative — a bad change restarts the process and could make things worse.
|
| 1450 |
-
- The Home Space repo is at the current working directory.
|
| 1451 |
-
- The key file is scripts/conversation-loop.py
|
| 1452 |
-
- Full monitoring context is also in GOD_CONTEXT.md"""
|
| 1453 |
|
| 1454 |
# 4. Set up env for Claude Code — prefer real Anthropic API, fall back to z.ai
|
| 1455 |
env = os.environ.copy()
|
|
@@ -1474,40 +1635,7 @@ You have the SAME capabilities as a human operator running Claude Code locally.
|
|
| 1474 |
print("[God] Using z.ai/Zhipu backend (set ANTHROPIC_API_KEY for real Claude)")
|
| 1475 |
env["CI"] = "true"
|
| 1476 |
|
| 1477 |
-
# 5.
|
| 1478 |
-
observations = []
|
| 1479 |
-
if _rate_limited:
|
| 1480 |
-
observations.append("Zhipu API is rate-limited, Adam & Eve turns are returning empty")
|
| 1481 |
-
if _discussion_loop_count >= 3:
|
| 1482 |
-
observations.append(f"discussion loop detected ({_discussion_loop_count} consecutive turns without task assignment)")
|
| 1483 |
-
elif _discussion_loop_count >= 1:
|
| 1484 |
-
observations.append(f"{_discussion_loop_count} turn(s) without task assignment")
|
| 1485 |
-
if cc_status.get("running"):
|
| 1486 |
-
observations.append(f"Claude Code is working on: {cc_status.get('task', '?')[:80]}")
|
| 1487 |
-
elif cc_status.get("result"):
|
| 1488 |
-
observations.append("Claude Code just finished a task, pending review")
|
| 1489 |
-
if child_state["stage"] not in ("RUNNING",):
|
| 1490 |
-
observations.append(f"{CHILD_NAME} is in stage: {child_state['stage']}")
|
| 1491 |
-
if not history:
|
| 1492 |
-
observations.append("no conversation history yet (fresh start)")
|
| 1493 |
-
elif len(history) <= 4:
|
| 1494 |
-
observations.append(f"only {len(history)} messages so far (early stage)")
|
| 1495 |
-
|
| 1496 |
-
if observations:
|
| 1497 |
-
obs_str = "; ".join(observations)
|
| 1498 |
-
start_en = f"Starting system review. Current observations: {obs_str}."
|
| 1499 |
-
start_zh = f"开始系统审查。当前观察:{obs_str}。"
|
| 1500 |
-
else:
|
| 1501 |
-
start_en = "Starting routine system review."
|
| 1502 |
-
start_zh = "开始例行系统审查。"
|
| 1503 |
-
ts_start = datetime.datetime.utcnow().strftime("%H:%M")
|
| 1504 |
-
entry_start = {"speaker": "God", "time": ts_start, "text": start_en, "text_zh": start_zh}
|
| 1505 |
-
history.append(entry_start)
|
| 1506 |
-
set_bubble(HOME, start_en, start_zh)
|
| 1507 |
-
post_chatlog(history)
|
| 1508 |
-
persist_turn("God", turn_count, start_en, start_zh, [], workflow_state, child_state["stage"])
|
| 1509 |
-
|
| 1510 |
-
# 6. Run Claude Code CLI
|
| 1511 |
print(f"[God] Starting Claude Code analysis...")
|
| 1512 |
t0 = time.time()
|
| 1513 |
try:
|
|
@@ -1544,16 +1672,7 @@ You have the SAME capabilities as a human operator running Claude Code locally.
|
|
| 1544 |
elapsed = time.time() - t0
|
| 1545 |
print(f"[God] Analysis complete ({elapsed:.1f}s, {len(output)} chars)")
|
| 1546 |
|
| 1547 |
-
#
|
| 1548 |
-
report_match = re.search(r'\[REPORT\]\s*(.+)', output)
|
| 1549 |
-
if report_match:
|
| 1550 |
-
report = report_match.group(1).strip()
|
| 1551 |
-
else:
|
| 1552 |
-
# Fallback: use last non-empty line of output
|
| 1553 |
-
non_empty = [l for l in output_lines if l.strip()] if output_lines else []
|
| 1554 |
-
report = non_empty[-1] if non_empty else "Analysis complete."
|
| 1555 |
-
|
| 1556 |
-
# Check if God pushed changes
|
| 1557 |
try:
|
| 1558 |
head_after = subprocess.run(
|
| 1559 |
"git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
|
|
@@ -1563,16 +1682,38 @@ You have the SAME capabilities as a human operator running Claude Code locally.
|
|
| 1563 |
except Exception:
|
| 1564 |
god_pushed = False
|
| 1565 |
|
|
|
|
| 1566 |
if god_pushed:
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
|
| 1572 |
-
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1576 |
|
| 1577 |
|
| 1578 |
_last_god_time = 0.0 # timestamp of last God run
|
|
|
|
| 42 |
"""
|
| 43 |
import json, time, re, requests, sys, os, io, subprocess, threading, datetime
|
| 44 |
from collections import deque
|
| 45 |
+
from pathlib import Path
|
| 46 |
|
| 47 |
# Force unbuffered output
|
| 48 |
sys.stdout.reconfigure(line_buffering=True)
|
|
|
|
| 384 |
CLAUDE_TIMEOUT = 300 # 5 minutes
|
| 385 |
TURN_INTERVAL = 15 # seconds between turns — fast enough for lively discussion
|
| 386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
+
def _write_claude_md(workspace, role="worker"):
|
| 389 |
+
"""Write CLAUDE.md to workspace so Claude Code loads persistent project knowledge.
|
| 390 |
+
|
| 391 |
+
This replaces stuffing static context into every prompt, saving tokens.
|
| 392 |
+
Claude Code reads CLAUDE.md automatically and builds its own memory in .claude/.
|
| 393 |
+
"""
|
| 394 |
+
if role == "worker":
|
| 395 |
+
content = f"""# HuggingClaw — {CHILD_NAME}'s Space
|
| 396 |
+
|
| 397 |
+
## Architecture
|
| 398 |
+
- {CHILD_NAME} is a child agent in the HuggingClaw World family system
|
| 399 |
+
- Runs as an OpenClaw instance on HuggingFace Spaces (sdk: docker, NOT gradio)
|
| 400 |
+
- Space ID: {CHILD_SPACE_ID}
|
| 401 |
+
- Dataset ID: {CHILD_DATASET_ID}
|
| 402 |
+
|
| 403 |
+
## Already Configured (DO NOT reconfigure these)
|
| 404 |
+
- HF_TOKEN — set as secret, working
|
| 405 |
+
- OPENCLAW_DATASET_REPO — set, pointing to {CHILD_NAME}'s dataset
|
| 406 |
+
- AUTO_CREATE_DATASET — set to true
|
| 407 |
+
- Docker port 7860
|
| 408 |
+
- sync_hf.py and entrypoint.sh are in place
|
| 409 |
+
|
| 410 |
+
## Technical Rules
|
| 411 |
+
- All Spaces use sdk: docker with Dockerfile-based deployment
|
| 412 |
+
- Docker containers MUST bind port 7860
|
| 413 |
+
- OOM (exit 137) = reduce dependencies or image size
|
| 414 |
+
- NEVER install torch/transformers unless absolutely required (2GB+, causes OOM)
|
| 415 |
+
- You have FULL permission to read/write/create/delete files. Just do it.
|
| 416 |
+
|
| 417 |
+
## Focus
|
| 418 |
+
Improve {CHILD_NAME}'s functionality, add features, fix bugs.
|
| 419 |
+
Do NOT re-check or re-configure infrastructure that is already working.
|
| 420 |
+
"""
|
| 421 |
+
elif role == "god":
|
| 422 |
+
content = f"""# HuggingClaw — System Supervisor (God)
|
| 423 |
+
|
| 424 |
+
## Your Role
|
| 425 |
+
You are God — the autonomous supervisor of the HuggingClaw family system.
|
| 426 |
+
You have the same capabilities as a human operator running Claude Code locally.
|
| 427 |
+
Your job: monitor Adam & Eve's conversation loop and fix mechanism issues.
|
| 428 |
|
| 429 |
+
## Architecture
|
| 430 |
+
- Home Space runs conversation-loop.py which orchestrates the family
|
| 431 |
+
- Adam & Eve converse via Zhipu GLM-4.5, assign [TASK] blocks to Claude Code CLI
|
| 432 |
+
- Claude Code worker clones Cain's repo, makes changes, and pushes
|
| 433 |
+
- You (God) monitor the conversation and fix the orchestration mechanism
|
| 434 |
+
- All Spaces use sdk: docker (NOT gradio)
|
| 435 |
+
|
| 436 |
+
## Rules
|
| 437 |
+
- ONLY modify scripts/conversation-loop.py — do NOT touch Cain's Space
|
| 438 |
+
- Only push fixes for real problems, not cosmetic or trivial changes
|
| 439 |
+
- Pushing triggers a Space restart — be confident the fix is correct
|
| 440 |
+
- If everything looks healthy, exit quickly without changes
|
| 441 |
+
|
| 442 |
+
## Common Issues to Watch For
|
| 443 |
+
- Agents repeating discussion about env vars that are already configured
|
| 444 |
+
- Discussion loops with no [TASK] assignment when CC is idle
|
| 445 |
+
- Rate limit handling issues
|
| 446 |
+
- System prompt not specific enough
|
| 447 |
+
- Action history not persisting across restarts
|
| 448 |
+
|
| 449 |
+
## Commit Convention
|
| 450 |
+
Always use: git commit -m "god: <brief description>"
|
| 451 |
+
"""
|
| 452 |
try:
|
| 453 |
+
with open(f"{workspace}/CLAUDE.md", "w") as f:
|
| 454 |
+
f.write(content)
|
| 455 |
+
except Exception as e:
|
| 456 |
+
print(f"[CLAUDE.md] Failed to write: {e}")
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def _reset_workspace(workspace, repo_url):
|
| 460 |
+
"""Reset workspace to latest origin/main, preserving .claude/ memory directory."""
|
| 461 |
+
try:
|
| 462 |
+
if os.path.exists(f"{workspace}/.git"):
|
| 463 |
try:
|
| 464 |
subprocess.run(
|
| 465 |
"git fetch origin && git reset --hard origin/main",
|
| 466 |
+
shell=True, cwd=workspace, timeout=30,
|
| 467 |
capture_output=True, check=True
|
| 468 |
)
|
| 469 |
except Exception:
|
| 470 |
+
# Preserve .claude/ memory if it exists
|
| 471 |
+
claude_dir = f"{workspace}/.claude"
|
| 472 |
+
has_memory = os.path.exists(claude_dir)
|
| 473 |
+
if has_memory:
|
| 474 |
+
subprocess.run(f"mv {claude_dir} /tmp/_claude_memory_bak", shell=True, capture_output=True)
|
| 475 |
+
subprocess.run(f"rm -rf {workspace}", shell=True, capture_output=True)
|
| 476 |
subprocess.run(
|
| 477 |
+
f"git clone --depth 20 {repo_url} {workspace}",
|
| 478 |
shell=True, timeout=60, capture_output=True, check=True
|
| 479 |
)
|
| 480 |
+
if has_memory:
|
| 481 |
+
subprocess.run(f"mv /tmp/_claude_memory_bak {claude_dir}", shell=True, capture_output=True)
|
| 482 |
else:
|
| 483 |
+
# Preserve .claude/ memory if workspace exists but is broken
|
| 484 |
+
claude_dir = f"{workspace}/.claude"
|
| 485 |
+
has_memory = os.path.exists(claude_dir)
|
| 486 |
+
if has_memory:
|
| 487 |
+
subprocess.run(f"mv {claude_dir} /tmp/_claude_memory_bak", shell=True, capture_output=True)
|
| 488 |
+
if os.path.exists(workspace):
|
| 489 |
+
subprocess.run(f"rm -rf {workspace}", shell=True, capture_output=True)
|
| 490 |
subprocess.run(
|
| 491 |
+
f"git clone --depth 20 {repo_url} {workspace}",
|
| 492 |
shell=True, timeout=60, capture_output=True, check=True
|
| 493 |
)
|
| 494 |
+
if has_memory:
|
| 495 |
+
subprocess.run(f"mv /tmp/_claude_memory_bak {claude_dir}", shell=True, capture_output=True)
|
| 496 |
+
subprocess.run(f'git config user.name "Claude Code"',
|
| 497 |
+
shell=True, cwd=workspace, capture_output=True)
|
| 498 |
+
subprocess.run(f'git config user.email "claude-code@huggingclaw"',
|
| 499 |
+
shell=True, cwd=workspace, capture_output=True)
|
| 500 |
+
return True
|
| 501 |
except Exception as e:
|
| 502 |
+
print(f"[WORKSPACE] Failed to prepare {workspace}: {e}")
|
| 503 |
+
return False
|
| 504 |
+
|
| 505 |
+
def action_claude_code(task):
|
| 506 |
+
"""Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
|
| 507 |
+
if not child_state["created"]:
|
| 508 |
+
return f"{CHILD_NAME} not born yet."
|
| 509 |
+
|
| 510 |
+
global _pending_cooldown
|
| 511 |
+
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{CHILD_SPACE_ID}"
|
| 512 |
+
|
| 513 |
+
# 1. Clone / reset to latest (preserving .claude/ memory)
|
| 514 |
+
if not _reset_workspace(CLAUDE_WORK_DIR, repo_url):
|
| 515 |
+
return "Failed to prepare workspace."
|
| 516 |
+
_write_claude_md(CLAUDE_WORK_DIR, role="worker")
|
| 517 |
|
| 518 |
# 2. Run Claude Code with z.ai backend (Zhipu GLM)
|
| 519 |
env = os.environ.copy()
|
|
|
|
| 716 |
|
| 717 |
|
| 718 |
def enrich_task_with_context(task_desc, ctx):
|
| 719 |
+
"""Append dynamic state to task. Static knowledge is in CLAUDE.md."""
|
| 720 |
parts = [task_desc]
|
| 721 |
+
# Only dynamic state — static knowledge (architecture, rules, env vars) is in CLAUDE.md
|
| 722 |
+
parts.append(f"\nCurrent stage: {child_state['stage']}")
|
|
|
|
|
|
|
|
|
|
| 723 |
parts.append(f"Health: {ctx.get('health', 'unknown')}")
|
|
|
|
| 724 |
return "\n".join(parts)
|
| 725 |
|
| 726 |
|
|
|
|
| 889 |
pass
|
| 890 |
|
| 891 |
|
| 892 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 893 |
+
# MODULE 4b: AGENT MEMORY (via OpenClaw workspace/memory/)
|
| 894 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 895 |
+
# Leverages OpenClaw's existing memory system:
|
| 896 |
+
# ~/.openclaw/workspace/memory/ — daily markdown files, auto-backed up by openclaw_persist.py
|
| 897 |
+
# Adam & Eve share a family conversation memory file that persists across restarts.
|
| 898 |
+
|
| 899 |
+
OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", "~/.openclaw")).expanduser()
|
| 900 |
+
FAMILY_MEMORY_DIR = OPENCLAW_HOME / "workspace" / "memory"
|
| 901 |
+
FAMILY_MEMORY_FILE = FAMILY_MEMORY_DIR / "family-conversation.md"
|
| 902 |
+
MAX_MEMORY_ENTRIES = 50 # keep memory focused
|
| 903 |
+
|
| 904 |
+
|
| 905 |
+
def _load_family_memory():
|
| 906 |
+
"""Load family conversation memory from OpenClaw workspace."""
|
| 907 |
+
try:
|
| 908 |
+
if FAMILY_MEMORY_FILE.exists():
|
| 909 |
+
content = FAMILY_MEMORY_FILE.read_text().strip()
|
| 910 |
+
if content:
|
| 911 |
+
return content
|
| 912 |
+
except Exception as e:
|
| 913 |
+
print(f"[MEMORY] Failed to load: {e}")
|
| 914 |
+
return ""
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
def _save_memory_entry(speaker, entry):
|
| 918 |
+
"""Append a memory entry to the family memory file."""
|
| 919 |
+
try:
|
| 920 |
+
FAMILY_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
| 921 |
+
timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
| 922 |
+
|
| 923 |
+
# Load existing entries
|
| 924 |
+
existing = ""
|
| 925 |
+
if FAMILY_MEMORY_FILE.exists():
|
| 926 |
+
existing = FAMILY_MEMORY_FILE.read_text().strip()
|
| 927 |
+
|
| 928 |
+
# Parse existing entries to enforce max limit
|
| 929 |
+
entries = []
|
| 930 |
+
if existing:
|
| 931 |
+
# Split by entry markers
|
| 932 |
+
for block in existing.split("\n- **"):
|
| 933 |
+
block = block.strip()
|
| 934 |
+
if block:
|
| 935 |
+
if not block.startswith("**"):
|
| 936 |
+
block = "**" + block
|
| 937 |
+
entries.append("- " + block)
|
| 938 |
+
|
| 939 |
+
# Add new entry
|
| 940 |
+
new_entry = f"- **[{timestamp}] {speaker}:** {entry}"
|
| 941 |
+
entries.append(new_entry)
|
| 942 |
+
|
| 943 |
+
# Trim to max
|
| 944 |
+
if len(entries) > MAX_MEMORY_ENTRIES:
|
| 945 |
+
entries = entries[-MAX_MEMORY_ENTRIES:]
|
| 946 |
+
|
| 947 |
+
# Write back with header
|
| 948 |
+
content = f"# Family Conversation Memory\n\n" + "\n".join(entries) + "\n"
|
| 949 |
+
FAMILY_MEMORY_FILE.write_text(content)
|
| 950 |
+
print(f"[MEMORY] {speaker} saved: {entry[:80]}")
|
| 951 |
+
except Exception as e:
|
| 952 |
+
print(f"[MEMORY] Failed to save: {e}")
|
| 953 |
+
|
| 954 |
+
|
| 955 |
+
def _parse_and_save_memories(speaker, text):
|
| 956 |
+
"""Parse [MEMORY: ...] tags from agent response and save them."""
|
| 957 |
+
memories = re.findall(r'\[MEMORY:\s*(.+?)\]', text)
|
| 958 |
+
for mem in memories:
|
| 959 |
+
_save_memory_entry(speaker, mem.strip())
|
| 960 |
+
return memories
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
def _format_memory_for_prompt():
|
| 964 |
+
"""Format family memory for injection into system prompt."""
|
| 965 |
+
mem = _load_family_memory()
|
| 966 |
+
if not mem:
|
| 967 |
+
return ""
|
| 968 |
+
# Truncate if too long (keep it under ~800 chars to save tokens)
|
| 969 |
+
if len(mem) > 800:
|
| 970 |
+
lines = mem.split("\n")
|
| 971 |
+
# Keep header + last N entries
|
| 972 |
+
truncated = [lines[0]] # header
|
| 973 |
+
total = len(lines[0])
|
| 974 |
+
for line in reversed(lines[1:]):
|
| 975 |
+
if total + len(line) > 750:
|
| 976 |
+
break
|
| 977 |
+
truncated.insert(1, line)
|
| 978 |
+
total += len(line)
|
| 979 |
+
mem = "\n".join(truncated)
|
| 980 |
+
return f"\n=== FAMILY MEMORY (persistent across restarts) ===\n{mem}\nTo save a new memory: [MEMORY: what you learned]\n"
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
# Initialize memory directory
|
| 984 |
+
try:
|
| 985 |
+
FAMILY_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
| 986 |
+
mem_count = _load_family_memory().count("- **")
|
| 987 |
+
print(f"[MEMORY] Loaded {mem_count} entries from {FAMILY_MEMORY_FILE}")
|
| 988 |
+
except Exception as e:
|
| 989 |
+
print(f"[MEMORY] Init warning: {e}")
|
| 990 |
+
|
| 991 |
+
|
| 992 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 993 |
# MODULE 5: TURN EXECUTION — Parse [TASK] and route to Claude Code
|
| 994 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 1177 |
if _discussion_loop_count >= 2:
|
| 1178 |
print(f"[LOOP-DISCUSS] WARNING: {_discussion_loop_count} consecutive discussion-only turns with CC IDLE and child alive!")
|
| 1179 |
|
| 1180 |
+
# Parse and save [MEMORY: ...] entries
|
| 1181 |
+
_parse_and_save_memories(_current_speaker, raw_text)
|
| 1182 |
+
|
| 1183 |
# Clean text for display
|
| 1184 |
clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
|
| 1185 |
+
clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean)
|
| 1186 |
+
clean = re.sub(r'\[MEMORY:[^\]]*\]', '', clean).strip()
|
| 1187 |
|
| 1188 |
return clean, results, task_assigned
|
| 1189 |
|
|
|
|
| 1296 |
[ACTION: create_child] — Create {CHILD_NAME} (if not born)
|
| 1297 |
[ACTION: terminate_cc] — Terminate a STUCK Claude Code process (use when CC has no new output for 180s+)
|
| 1298 |
|
| 1299 |
+
MEMORY:
|
| 1300 |
+
- You have persistent memory that survives restarts. Check the FAMILY MEMORY section in context.
|
| 1301 |
+
- To save an important learning or decision: [MEMORY: what you learned]
|
| 1302 |
+
- Examples: [MEMORY: Cain's Dockerfile needs port 7860 binding], [MEMORY: torch causes OOM on free tier]
|
| 1303 |
+
- Only save genuinely useful insights — not routine observations.
|
| 1304 |
+
|
| 1305 |
HF SPACES TECHNICAL NOTES:
|
| 1306 |
- We use sdk: docker (NOT gradio). All Spaces run via Dockerfile.
|
| 1307 |
- Docker containers MUST bind port 7860.
|
|
|
|
| 1376 |
parts.append(f"YOU MUST write a [TASK]...[/TASK] block NOW. Do NOT write another discussion response.")
|
| 1377 |
parts.append(f"Examples of tasks: 'Check the logs', 'Read config.py', 'Add a feature', 'Fix a bug', etc.")
|
| 1378 |
|
| 1379 |
+
# Family memory (persistent across restarts)
|
| 1380 |
+
mem_section = _format_memory_for_prompt()
|
| 1381 |
+
if mem_section:
|
| 1382 |
+
parts.append(mem_section)
|
| 1383 |
+
|
| 1384 |
parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
|
| 1385 |
parts.append("English first, then --- separator, then Chinese translation.")
|
| 1386 |
|
|
|
|
| 1576 |
"""
|
| 1577 |
global last_action_results
|
| 1578 |
|
| 1579 |
+
# 1. Clone/update Home Space repo (preserving .claude/ memory)
|
| 1580 |
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{HOME_SPACE_ID}"
|
| 1581 |
+
if not _reset_workspace(GOD_WORK_DIR, repo_url):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
return
|
| 1583 |
+
_write_claude_md(GOD_WORK_DIR, role="god")
|
| 1584 |
|
| 1585 |
# Record HEAD before Claude Code runs (to detect if God pushed changes)
|
| 1586 |
try:
|
|
|
|
| 1599 |
except Exception as e:
|
| 1600 |
print(f"[God] Warning: Could not write context file: {e}")
|
| 1601 |
|
| 1602 |
+
# 3. Build God's prompt — only dynamic state; static knowledge is in CLAUDE.md
|
| 1603 |
+
prompt = f"""## Current System State
|
|
|
|
|
|
|
|
|
|
| 1604 |
{context}
|
| 1605 |
|
| 1606 |
+
## Tasks
|
| 1607 |
+
1. Analyze the conversation. Progress or stuck?
|
| 1608 |
+
2. If stuck, diagnose root cause in scripts/conversation-loop.py
|
| 1609 |
+
3. Fix and push if needed (commit with "god: <description>")
|
| 1610 |
+
4. If you made changes, end with BOTH of these lines:
|
| 1611 |
+
[PROBLEM] <what the problem was>
|
| 1612 |
+
[FIX] <what you changed to fix it>
|
| 1613 |
+
5. If no changes needed, end with: [OK] system is healthy"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
|
| 1615 |
# 4. Set up env for Claude Code — prefer real Anthropic API, fall back to z.ai
|
| 1616 |
env = os.environ.copy()
|
|
|
|
| 1635 |
print("[God] Using z.ai/Zhipu backend (set ANTHROPIC_API_KEY for real Claude)")
|
| 1636 |
env["CI"] = "true"
|
| 1637 |
|
| 1638 |
+
# 5. Run Claude Code CLI (no chatlog announcement — God only speaks when making changes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1639 |
print(f"[God] Starting Claude Code analysis...")
|
| 1640 |
t0 = time.time()
|
| 1641 |
try:
|
|
|
|
| 1672 |
elapsed = time.time() - t0
|
| 1673 |
print(f"[God] Analysis complete ({elapsed:.1f}s, {len(output)} chars)")
|
| 1674 |
|
| 1675 |
+
# 6. Check if God pushed changes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1676 |
try:
|
| 1677 |
head_after = subprocess.run(
|
| 1678 |
"git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
|
|
|
|
| 1682 |
except Exception:
|
| 1683 |
god_pushed = False
|
| 1684 |
|
| 1685 |
+
# 7. Only post to chatlog if God made changes
|
| 1686 |
if god_pushed:
|
| 1687 |
+
# Parse [PROBLEM] and [FIX] from output
|
| 1688 |
+
problem_match = re.search(r'\[PROBLEM\]\s*(.+)', output)
|
| 1689 |
+
fix_match = re.search(r'\[FIX\]\s*(.+)', output)
|
| 1690 |
+
|
| 1691 |
+
problem_text = problem_match.group(1).strip() if problem_match else ""
|
| 1692 |
+
fix_text = fix_match.group(1).strip() if fix_match else ""
|
| 1693 |
+
|
| 1694 |
+
if problem_text and fix_text:
|
| 1695 |
+
msg_en = f"Found issue: {problem_text}. Fixed: {fix_text}. System will restart shortly."
|
| 1696 |
+
msg_zh = msg_en # God speaks in English for now
|
| 1697 |
+
elif fix_text:
|
| 1698 |
+
msg_en = f"Fixed: {fix_text}. System will restart shortly."
|
| 1699 |
+
msg_zh = msg_en
|
| 1700 |
+
else:
|
| 1701 |
+
# Fallback: use last non-empty lines
|
| 1702 |
+
non_empty = [l for l in output_lines if l.strip()] if output_lines else []
|
| 1703 |
+
fallback = non_empty[-1] if non_empty else "Applied a fix."
|
| 1704 |
+
msg_en = f"{fallback} System will restart shortly."
|
| 1705 |
+
msg_zh = msg_en
|
| 1706 |
+
|
| 1707 |
+
ts_end = datetime.datetime.utcnow().strftime("%H:%M")
|
| 1708 |
+
entry_end = {"speaker": "God", "time": ts_end, "text": msg_en, "text_zh": msg_zh}
|
| 1709 |
+
history.append(entry_end)
|
| 1710 |
+
set_bubble(HOME, msg_en[:200], msg_zh[:200])
|
| 1711 |
+
post_chatlog(history)
|
| 1712 |
+
persist_turn("God", turn_count, msg_en, msg_zh, [], workflow_state, child_state["stage"])
|
| 1713 |
+
print(f"[God] Posted fix: {msg_en}")
|
| 1714 |
+
else:
|
| 1715 |
+
# No changes — silent, just log locally
|
| 1716 |
+
print(f"[God] No changes needed, staying silent.")
|
| 1717 |
|
| 1718 |
|
| 1719 |
_last_god_time = 0.0 # timestamp of last God run
|