tao-shen Claude Opus 4.6 commited on
Commit
545b0e0
·
1 Parent(s): 5af3f44

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>

Files changed (1) hide show
  1. 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
- global _pending_cooldown
392
- repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{CHILD_SPACE_ID}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
- # 1. Clone / reset to latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  try:
396
- if os.path.exists(f"{CLAUDE_WORK_DIR}/.git"):
 
 
 
 
 
 
 
 
 
397
  try:
398
  subprocess.run(
399
  "git fetch origin && git reset --hard origin/main",
400
- shell=True, cwd=CLAUDE_WORK_DIR, timeout=30,
401
  capture_output=True, check=True
402
  )
403
  except Exception:
404
- subprocess.run(f"rm -rf {CLAUDE_WORK_DIR}", shell=True, capture_output=True)
 
 
 
 
 
405
  subprocess.run(
406
- f"git clone --depth 20 {repo_url} {CLAUDE_WORK_DIR}",
407
  shell=True, timeout=60, capture_output=True, check=True
408
  )
 
 
409
  else:
410
- if os.path.exists(CLAUDE_WORK_DIR):
411
- subprocess.run(f"rm -rf {CLAUDE_WORK_DIR}", shell=True, capture_output=True)
 
 
 
 
 
412
  subprocess.run(
413
- f"git clone --depth 20 {repo_url} {CLAUDE_WORK_DIR}",
414
  shell=True, timeout=60, capture_output=True, check=True
415
  )
416
- subprocess.run('git config user.name "Claude Code"',
417
- shell=True, cwd=CLAUDE_WORK_DIR, capture_output=True)
418
- subprocess.run('git config user.email "claude-code@huggingclaw"',
419
- shell=True, cwd=CLAUDE_WORK_DIR, capture_output=True)
 
 
 
420
  except Exception as e:
421
- return f"Failed to prepare workspace: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 auto-gathered context to task description for Claude Code."""
625
  parts = [task_desc]
626
- parts.append(f"\n\nIMPORTANT: You have FULL permission to read and write files. Do NOT ask for permission. Just make the changes directly. Edit files, create files, delete files — whatever is needed. Do NOT output code suggestions — actually write them to the files.")
627
- parts.append(f"\n--- AUTO-GATHERED CONTEXT ---")
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).strip()
 
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
- try:
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"""You are God — the autonomous supervisor of the HuggingClaw family system.
1416
- You have the SAME capabilities as a human operator running Claude Code locally.
1417
-
1418
- ## Current System State
1419
  {context}
1420
 
1421
- ## Your Mission
1422
- 1. ANALYZE: Read the conversation above. Are Adam & Eve making real progress or stuck in loops?
1423
- Signs of trouble: repeating the same discussion topics, discussing env vars that are already set,
1424
- failing to assign [TASK] blocks when CC is idle, rate limit spinning.
1425
- 2. DIAGNOSE: If you find problems, read scripts/conversation-loop.py to understand the mechanism
1426
- and identify the root cause. Focus on system prompts, loop detection, action history.
1427
- 3. FIX: Edit scripts/conversation-loop.py to fix the issue. Common fixes:
1428
- - Strengthen system prompts to prevent repetitive discussions
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. Announce analysis startdescribe current observations dynamically
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
- # 7. Parse [REPORT] from God's output and post to chatlog
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
- report += " System will restart shortly to apply changes."
1568
-
1569
- ts_end = datetime.datetime.utcnow().strftime("%H:%M")
1570
- entry_end = {"speaker": "God", "time": ts_end, "text": report, "text_zh": report}
1571
- history.append(entry_end)
1572
- set_bubble(HOME, report[:200], report[:200])
1573
- post_chatlog(history)
1574
- persist_turn("God", turn_count, report, report, [], workflow_state, child_state["stage"])
1575
- print(f"[God] Report: {report}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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