Cyber Catalyst Team commited on
Commit
ed6da33
·
1 Parent(s): 172babb

feat: async httpx inter-space calls, Karpathy project_state.md, vault push, idle worker protocol

Browse files
__pycache__/backend.cpython-310.pyc ADDED
Binary file (76.6 kB). View file
 
backend.py CHANGED
@@ -1748,14 +1748,20 @@ class ForgeExecuteRequest(BaseModel):
1748
  action: str
1749
  prompt: str
1750
  context_rules: Optional[str] = ""
 
1751
 
1752
  @app.post("/api/forge/execute")
1753
  async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(None)):
1754
  auth(authorization)
1755
- log_activity(f"[Forge] Received execution request for task {req.task_id}: '{req.prompt}'")
1756
  try:
 
 
 
 
 
1757
  messages = [
1758
- {"role": "system", "content": AGENTIC_SYSTEM_PROMPT + f"\nContext rules: {req.context_rules}"},
1759
  {"role": "user", "content": req.prompt}
1760
  ]
1761
 
@@ -1861,9 +1867,20 @@ async def db_heartbeat_loop():
1861
  await asyncio.sleep(240) # Every 4 minutes
1862
 
1863
 
 
 
 
 
 
 
 
 
 
 
 
1864
  SPACE3_URL = os.environ.get("SPACE3_URL", "https://augment17-claude-code-backend.hf.space")
1865
  SPACE4_URL = os.environ.get("SPACE4_URL", "https://shyota-mcp-cloud-host.hf.space")
1866
- SPACE5_URL = os.environ.get("SPACE5_URL", "https://augment17-better-chatbot.hf.space")
1867
  SPACE6_URL = os.environ.get("SPACE6_URL", "https://augment17-mcp-cloud-host.hf.space")
1868
 
1869
  async def get_active_projects():
@@ -1882,117 +1899,232 @@ async def update_db_roadmap(project_name: str, roadmap: list):
1882
  async with db_pool.acquire() as conn:
1883
  await conn.execute("UPDATE eternity_projects SET roadmap = $1 WHERE project_name = $2", json.dumps(roadmap), project_name)
1884
 
1885
- def post_json(url: str, payload: dict) -> dict:
 
1886
  headers = {
1887
  "Authorization": f"Bearer {BACKEND_API_KEY}",
1888
  "Content-Type": "application/json"
1889
  }
1890
- req = urllib.request.Request(
1891
- url,
1892
- data=json.dumps(payload).encode('utf-8'),
1893
- headers=headers,
1894
- method="POST"
1895
- )
1896
  try:
1897
- with urllib.request.urlopen(req, timeout=120) as r:
1898
- return json.loads(r.read().decode('utf-8'))
 
 
1899
  except Exception as e:
1900
- print(f"[HTTP Error] POST to {url} failed: {e}")
1901
  return {"status": "error", "error": str(e)}
1902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1903
  async def execute_build_cycle(project_name: str, goal: str):
1904
  log_activity(f"[Build Mode] Initiating build cycle for project '{project_name}' (goal: '{goal}')")
1905
  await rate_limiter.wait_for_nim()
1906
- prompt = f"We are building: '{goal}'. Write a JSON instruction for Space 3 (The Forge) to code the next milestone. Respond ONLY with JSON matching the contract: " + '{"prompt": "task description", "context_rules": "rules"}'
 
 
 
 
1907
  try:
1908
  res = await nim_client.chat.completions.create(
1909
  model="nvidia/llama-3.1-nemotron-70b-instruct",
1910
- messages=[{"role": "user", "content": prompt}],
1911
  max_tokens=300
1912
  )
1913
- task = json.loads(res.choices[0].message.content.strip())
1914
- task_prompt = task.get("prompt")
 
 
 
 
1915
  context_rules = task.get("context_rules", "")
1916
  except Exception as e:
1917
  log_activity(f"[Build Mode Error] NIM planning failed for project '{project_name}': {e}")
1918
  return
1919
 
1920
- log_activity(f"[Build Mode] Dispatching project '{project_name}' task to Space 3: '{task_prompt}'")
1921
- forge_res = post_json(f"{SPACE3_URL}/api/forge/execute", {
 
 
 
 
 
 
1922
  "task_id": f"build_{project_name}_{int(time.time())}",
1923
  "action": "execute_code",
1924
  "prompt": task_prompt,
1925
- "context_rules": context_rules
 
1926
  })
1927
-
1928
  if forge_res.get("status") == "success":
1929
- log_activity(f"[Build Mode] Space 3 success for project '{project_name}': {forge_res.get('summary')}")
1930
- log_activity(f"[Build Mode] Triggering Space 6 (The Sandbox) UI verification for project '{project_name}'...")
1931
- test_res = post_json(f"{SPACE6_URL}/api/sandbox/test", {
 
 
1932
  "test_cmd": "verify_ui",
1933
- "url": f"{SPACE3_URL}"
1934
- })
1935
- log_activity(f"[Build Mode] Space 6 Test Verdict: {test_res.get('verdict')} | Reason: {test_res.get('reason')}")
1936
-
1937
- log_activity("[Build Mode] Triggering Space 5 (The Vault) backup commit...")
1938
- post_json(f"{SPACE5_URL}/api/vault/push", {})
 
 
 
 
 
 
 
1939
  else:
1940
- log_activity(f"[Build Mode Warning] Space 3 reported failure: {forge_res.get('error')}")
1941
 
1942
  async def execute_eternity_cycle(project_name: str, goal: str):
1943
- log_activity(f"[Eternity Mode] Initiating autonomous R&D cycle for project '{project_name}' (goal: '{goal}')")
1944
- log_activity(f"[Eternity Mode] Querying Space 4 (The Library) for research on project '{project_name}'...")
1945
- research_res = post_json(f"{SPACE4_URL}/api/research", {"query": f"novel algorithms and components for {goal}"})
 
 
 
 
1946
  brief = research_res.get("brief", "No new features found.")
1947
  await update_db_brief(project_name, brief)
1948
- log_activity(f"[Eternity Mode] Received research brief for '{project_name}': {brief[:100]}...")
1949
 
 
1950
  await rate_limiter.wait_for_nim()
1951
- prompt = f"Goal: '{goal}'. Research Brief: '{brief}'. Plan the next feature/optimization code. Respond ONLY with JSON: " + '{"prompt": "task description", "context_rules": "rules"}'
 
 
 
 
1952
  try:
1953
  res = await nim_client.chat.completions.create(
1954
  model="nvidia/llama-3.1-nemotron-70b-instruct",
1955
- messages=[{"role": "user", "content": prompt}],
1956
  max_tokens=300
1957
  )
1958
- task = json.loads(res.choices[0].message.content.strip())
1959
- task_prompt = task.get("prompt")
 
 
 
1960
  context_rules = task.get("context_rules", "")
1961
  except Exception as e:
1962
- log_activity(f"[Eternity Mode Error] NIM planning failed for project '{project_name}': {e}")
1963
  return
1964
 
1965
- log_activity(f"[Eternity Mode] Dispatching task to Space 3: '{task_prompt}'")
1966
- forge_res = post_json(f"{SPACE3_URL}/api/forge/execute", {
 
 
 
 
 
 
 
1967
  "task_id": f"eternity_{project_name}_{int(time.time())}",
1968
  "action": "execute_code",
1969
  "prompt": task_prompt,
1970
- "context_rules": context_rules
 
1971
  })
1972
 
1973
  if forge_res.get("status") == "success":
1974
- log_activity(f"[Eternity Mode] Space 3 success: {forge_res.get('summary')}")
1975
- log_activity("[Eternity Mode] Triggering Space 5 (The Vault) backup commit...")
1976
- post_json(f"{SPACE5_URL}/api/vault/push", {})
1977
-
1978
-
1979
- last_run_times = {} # project_name -> timestamp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1980
 
1981
  async def run_eternity_loop():
1982
- log_activity("[Eternity Loop] Async task started on main loop.")
 
1983
  loop_counter = 0
1984
  while True:
1985
  try:
1986
  if not db_pool:
1987
  await asyncio.sleep(10)
1988
  continue
1989
-
1990
  projects = await get_active_projects()
1991
-
1992
  if not projects:
1993
- await asyncio.sleep(30)
 
 
 
 
 
1994
  continue
1995
-
1996
  loop_counter += 1
1997
  for p in projects:
1998
  name = p["project_name"]
@@ -2000,35 +2132,36 @@ async def run_eternity_loop():
2000
  deadline = p["deadline"]
2001
  current_mode = p["current_mode"]
2002
  priority = p["priority"]
2003
-
2004
- # Priority gating: Low priority skips 3 out of 4 cycles to save token rate limits
2005
  if priority == "low" and (loop_counter % 4) != 0:
 
2006
  continue
2007
-
2008
  now = datetime.now(timezone.utc)
2009
  if current_mode == "build" and now >= deadline:
2010
  await update_db_mode(name, "eternity")
2011
  current_mode = "eternity"
2012
- log_activity(f"[Eternity Loop] Project '{name}' deadline reached. Transitioned to Eternity R&D Mode.")
2013
-
2014
  if current_mode == "build":
2015
  await execute_build_cycle(name, goal)
2016
  else:
2017
- # Enforce the sleep interval for Eternity Mode independently per project
2018
  last_run = last_run_times.get(name, 0.0)
2019
  now_ts = time.time()
2020
  interval = int(os.environ.get("ETERNITY_LOOP_INTERVAL", "3600"))
2021
  if now_ts - last_run < interval:
 
 
2022
  continue
2023
  last_run_times[name] = now_ts
2024
-
2025
  await execute_eternity_cycle(name, goal)
2026
-
2027
- # Base check interval (5 minutes)
2028
  await asyncio.sleep(300)
2029
-
2030
  except Exception as e:
2031
- log_activity(f"[Eternity Loop Error] Loop crash: {e}")
2032
  await asyncio.sleep(60)
2033
 
2034
 
@@ -2037,6 +2170,42 @@ async def dashboard():
2037
  return HTMLResponse(content=DASHBOARD_HTML)
2038
 
2039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2040
  @app.get("/api/logs")
2041
  async def get_logs():
2042
  return list(activity_logs)
 
1748
  action: str
1749
  prompt: str
1750
  context_rules: Optional[str] = ""
1751
+ project_state_md: Optional[str] = "" # Karpathy CLAUDE.md protocol — shared project context
1752
 
1753
  @app.post("/api/forge/execute")
1754
  async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(None)):
1755
  auth(authorization)
1756
+ log_activity(f"[Forge] Received execution request for task {req.task_id}: '{req.prompt[:80]}'")
1757
  try:
1758
+ # Inject project_state.md into system context if provided (Karpathy protocol)
1759
+ system_content = AGENTIC_SYSTEM_PROMPT + f"\nContext rules: {req.context_rules}"
1760
+ if req.project_state_md:
1761
+ system_content += f"\n\n---\n## Project State Document (read this first)\n{req.project_state_md}"
1762
+
1763
  messages = [
1764
+ {"role": "system", "content": system_content},
1765
  {"role": "user", "content": req.prompt}
1766
  ]
1767
 
 
1867
  await asyncio.sleep(240) # Every 4 minutes
1868
 
1869
 
1870
+ import httpx
1871
+
1872
+ # ---------------------------------------------------------------------------
1873
+ # Inter-Space URLs — The 6-Space Topology
1874
+ # Space 2 (self) = shyota/claude-code-backend — Cerebrum (Orchestrator)
1875
+ # Space 3 = augment17/claude-code-backend — Forge (Coder)
1876
+ # Space 4 = shyota/mcp-cloud-host — Library (Research + MCP)
1877
+ # Space 5 (Vault) = Space 3 also handles vault push since agent-worker-loop
1878
+ # HF slot is not provisioned. SPACE5_URL = SPACE3_URL.
1879
+ # Space 6 = augment17/mcp-cloud-host — Sandbox (UI Tester)
1880
+ # ---------------------------------------------------------------------------
1881
  SPACE3_URL = os.environ.get("SPACE3_URL", "https://augment17-claude-code-backend.hf.space")
1882
  SPACE4_URL = os.environ.get("SPACE4_URL", "https://shyota-mcp-cloud-host.hf.space")
1883
+ SPACE5_URL = os.environ.get("SPACE5_URL", SPACE3_URL) # Vault is co-hosted on Space 3
1884
  SPACE6_URL = os.environ.get("SPACE6_URL", "https://augment17-mcp-cloud-host.hf.space")
1885
 
1886
  async def get_active_projects():
 
1899
  async with db_pool.acquire() as conn:
1900
  await conn.execute("UPDATE eternity_projects SET roadmap = $1 WHERE project_name = $2", json.dumps(roadmap), project_name)
1901
 
1902
+ # Async HTTP POST replaces blocking urllib.request so the event loop never freezes
1903
+ async def apost_json(url: str, payload: dict, timeout: float = 120.0) -> dict:
1904
  headers = {
1905
  "Authorization": f"Bearer {BACKEND_API_KEY}",
1906
  "Content-Type": "application/json"
1907
  }
 
 
 
 
 
 
1908
  try:
1909
+ async with httpx.AsyncClient(timeout=timeout) as client:
1910
+ r = await client.post(url, json=payload, headers=headers)
1911
+ r.raise_for_status()
1912
+ return r.json()
1913
  except Exception as e:
1914
+ log_activity(f"[HTTP Error] POST to {url} failed: {e}")
1915
  return {"status": "error", "error": str(e)}
1916
 
1917
+ # ---------------------------------------------------------------------------
1918
+ # Karpathy project_state.md — Shared Context Document
1919
+ # Each space receives this instead of bare prompts so agents always know
1920
+ # what phase they are in, what was built before, and what the research says.
1921
+ # ---------------------------------------------------------------------------
1922
+ def build_project_state_md(project_name: str, goal: str, mode: str,
1923
+ task_prompt: str, brief: str = "",
1924
+ prior_summary: str = "") -> str:
1925
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%MZ")
1926
+ return f"""# Project State: {project_name}
1927
+ _Generated by Space 2 (Cerebrum) at {ts}_
1928
+
1929
+ ## Current Objective
1930
+ **Mode:** {mode.upper()}
1931
+ **Goal:** {goal}
1932
+
1933
+ ## Instructions for Space 3 (The Forge)
1934
+ {task_prompt}
1935
+
1936
+ ## Research Brief (from Space 4 — The Library)
1937
+ {brief if brief else '_No research brief yet._'}
1938
+
1939
+ ## Prior Work Summary
1940
+ {prior_summary if prior_summary else '_First cycle._'}
1941
+
1942
+ ## Context Rules
1943
+ - Write deterministic, testable code.
1944
+ - All files go in /tmp/workspace/.
1945
+ - Return a 3-line summary: status, file changed, test result.
1946
+ """
1947
+
1948
  async def execute_build_cycle(project_name: str, goal: str):
1949
  log_activity(f"[Build Mode] Initiating build cycle for project '{project_name}' (goal: '{goal}')")
1950
  await rate_limiter.wait_for_nim()
1951
+ plan_prompt = (
1952
+ f"We are building: '{goal}'. "
1953
+ "Write ONE JSON instruction for Space 3 (The Forge) to code the next milestone. "
1954
+ "Respond ONLY with JSON: {\"prompt\": \"task description\", \"context_rules\": \"rules\"}"
1955
+ )
1956
  try:
1957
  res = await nim_client.chat.completions.create(
1958
  model="nvidia/llama-3.1-nemotron-70b-instruct",
1959
+ messages=[{"role": "user", "content": plan_prompt}],
1960
  max_tokens=300
1961
  )
1962
+ raw = res.choices[0].message.content.strip()
1963
+ # Strip markdown code fences if present
1964
+ if raw.startswith("```"):
1965
+ raw = raw.split("```")[1].lstrip("json").strip()
1966
+ task = json.loads(raw)
1967
+ task_prompt = task.get("prompt", "")
1968
  context_rules = task.get("context_rules", "")
1969
  except Exception as e:
1970
  log_activity(f"[Build Mode Error] NIM planning failed for project '{project_name}': {e}")
1971
  return
1972
 
1973
+ # Build the Karpathy project_state.md to send to Space 3
1974
+ state_md = build_project_state_md(
1975
+ project_name=project_name, goal=goal, mode="build",
1976
+ task_prompt=task_prompt
1977
+ )
1978
+
1979
+ log_activity(f"[Build Mode] Dispatching to Space 3 (Forge) — project '{project_name}': '{task_prompt[:80]}'")
1980
+ forge_res = await apost_json(f"{SPACE3_URL}/api/forge/execute", {
1981
  "task_id": f"build_{project_name}_{int(time.time())}",
1982
  "action": "execute_code",
1983
  "prompt": task_prompt,
1984
+ "context_rules": context_rules,
1985
+ "project_state_md": state_md # Karpathy CLAUDE.md protocol
1986
  })
1987
+
1988
  if forge_res.get("status") == "success":
1989
+ summary = forge_res.get("summary", "")
1990
+ log_activity(f"[Build Mode] Space 3 (Forge) SUCCESS for '{project_name}': {summary}")
1991
+
1992
+ log_activity(f"[Build Mode] Triggering Space 6 (Sandbox) UI verification for '{project_name}'...")
1993
+ test_res = await apost_json(f"{SPACE6_URL}/api/sandbox/test", {
1994
  "test_cmd": "verify_ui",
1995
+ "url": SPACE3_URL,
1996
+ "project_name": project_name
1997
+ }, timeout=30.0)
1998
+ verdict = test_res.get("verdict", "UNKNOWN")
1999
+ reason = test_res.get("reason", "")
2000
+ log_activity(f"[Build Mode] Space 6 (Sandbox) Verdict: {verdict} {reason}")
2001
+
2002
+ log_activity("[Build Mode] Triggering Space 5 (Vault) backup commit...")
2003
+ vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2004
+ "project_name": project_name,
2005
+ "summary": summary
2006
+ }, timeout=30.0)
2007
+ log_activity(f"[Build Mode] Space 5 (Vault) backup: {vault_res.get('status', 'unknown')}")
2008
  else:
2009
+ log_activity(f"[Build Mode Warning] Space 3 (Forge) failure for '{project_name}': {forge_res.get('error')}")
2010
 
2011
  async def execute_eternity_cycle(project_name: str, goal: str):
2012
+ log_activity(f"[Eternity Mode] R&D cycle starting for project '{project_name}' (goal: '{goal}')")
2013
+
2014
+ # Step 1: Ask Space 4 (Library) for research
2015
+ log_activity(f"[Eternity Mode] Querying Space 4 (Library) for '{project_name}'...")
2016
+ research_res = await apost_json(f"{SPACE4_URL}/api/research", {
2017
+ "query": f"novel algorithms, optimizations, and next features for: {goal}"
2018
+ })
2019
  brief = research_res.get("brief", "No new features found.")
2020
  await update_db_brief(project_name, brief)
2021
+ log_activity(f"[Eternity Mode] Space 4 (Library) brief received ({len(brief)} chars)")
2022
 
2023
+ # Step 2: Ask NIM to plan next feature based on research
2024
  await rate_limiter.wait_for_nim()
2025
+ plan_prompt = (
2026
+ f"Goal: '{goal}'.\nResearch Brief:\n{brief[:2000]}\n\n"
2027
+ "Based on this research, plan the SINGLE most impactful next feature to implement. "
2028
+ "Respond ONLY with JSON: {\"prompt\": \"task\", \"context_rules\": \"rules\"}"
2029
+ )
2030
  try:
2031
  res = await nim_client.chat.completions.create(
2032
  model="nvidia/llama-3.1-nemotron-70b-instruct",
2033
+ messages=[{"role": "user", "content": plan_prompt}],
2034
  max_tokens=300
2035
  )
2036
+ raw = res.choices[0].message.content.strip()
2037
+ if raw.startswith("```"):
2038
+ raw = raw.split("```")[1].lstrip("json").strip()
2039
+ task = json.loads(raw)
2040
+ task_prompt = task.get("prompt", "")
2041
  context_rules = task.get("context_rules", "")
2042
  except Exception as e:
2043
+ log_activity(f"[Eternity Mode Error] NIM planning failed for '{project_name}': {e}")
2044
  return
2045
 
2046
+ # Step 3: Build Karpathy project_state.md
2047
+ state_md = build_project_state_md(
2048
+ project_name=project_name, goal=goal, mode="eternity",
2049
+ task_prompt=task_prompt, brief=brief
2050
+ )
2051
+
2052
+ # Step 4: Dispatch to Space 3 (Forge)
2053
+ log_activity(f"[Eternity Mode] Dispatching to Space 3 (Forge): '{task_prompt[:80]}'")
2054
+ forge_res = await apost_json(f"{SPACE3_URL}/api/forge/execute", {
2055
  "task_id": f"eternity_{project_name}_{int(time.time())}",
2056
  "action": "execute_code",
2057
  "prompt": task_prompt,
2058
+ "context_rules": context_rules,
2059
+ "project_state_md": state_md
2060
  })
2061
 
2062
  if forge_res.get("status") == "success":
2063
+ summary = forge_res.get("summary", "")
2064
+ log_activity(f"[Eternity Mode] Space 3 (Forge) SUCCESS: {summary}")
2065
+ # Trigger vault backup after each successful eternity cycle
2066
+ vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2067
+ "project_name": project_name,
2068
+ "summary": summary
2069
+ }, timeout=30.0)
2070
+ log_activity(f"[Eternity Mode] Space 5 (Vault) backup: {vault_res.get('status', 'unknown')}")
2071
+ else:
2072
+ log_activity(f"[Eternity Mode Warning] Space 3 (Forge) failure: {forge_res.get('error')}")
2073
+
2074
+
2075
+ last_run_times = {} # project_name -> last execution timestamp
2076
+ IDLE_TASK_INTERVAL = 1800 # Run idle tasks every 30 min when no active projects
2077
+ _last_idle_run = 0.0
2078
+
2079
+ async def execute_idle_worker_cycle():
2080
+ """Idle Worker Protocol — runs when no active projects exist.
2081
+ Dispatches self-improvement tasks to Space 3 (Forge) to keep
2082
+ all containers warm and utilise free compute."""
2083
+ log_activity("[Idle Worker] No active projects. Dispatching background optimisation task to Space 3...")
2084
+ idle_tasks = [
2085
+ "Scan /tmp/workspace for any Python or JS files. For each file found, "
2086
+ "check if a corresponding test file exists. If not, write one basic unit test and run it. "
2087
+ "Return a summary of files scanned and tests added.",
2088
+ "Scan /tmp/workspace for duplicate function definitions across files. "
2089
+ "Report any found and refactor the worst one to a shared utility module.",
2090
+ "Run a syntax check on all files in /tmp/workspace and fix any issues found.",
2091
+ ]
2092
+ import random
2093
+ chosen = random.choice(idle_tasks)
2094
+ state_md = build_project_state_md(
2095
+ project_name="idle-maintenance", goal="system self-improvement",
2096
+ mode="idle", task_prompt=chosen
2097
+ )
2098
+ res = await apost_json(f"{SPACE3_URL}/api/forge/execute", {
2099
+ "task_id": f"idle_{int(time.time())}",
2100
+ "action": "execute_code",
2101
+ "prompt": chosen,
2102
+ "context_rules": "Be concise. Fix only what is necessary. Return a 3-line summary.",
2103
+ "project_state_md": state_md
2104
+ }, timeout=60.0)
2105
+ log_activity(f"[Idle Worker] Space 3 response: {res.get('status')} — {str(res.get('summary', ''))[:120]}")
2106
 
2107
  async def run_eternity_loop():
2108
+ global _last_idle_run
2109
+ log_activity("[Eternity Loop] Async orchestration task started on FastAPI main loop.")
2110
  loop_counter = 0
2111
  while True:
2112
  try:
2113
  if not db_pool:
2114
  await asyncio.sleep(10)
2115
  continue
2116
+
2117
  projects = await get_active_projects()
2118
+
2119
  if not projects:
2120
+ # --- Idle Worker Protocol ---
2121
+ now_ts = time.time()
2122
+ if now_ts - _last_idle_run >= IDLE_TASK_INTERVAL:
2123
+ _last_idle_run = now_ts
2124
+ await execute_idle_worker_cycle()
2125
+ await asyncio.sleep(60)
2126
  continue
2127
+
2128
  loop_counter += 1
2129
  for p in projects:
2130
  name = p["project_name"]
 
2132
  deadline = p["deadline"]
2133
  current_mode = p["current_mode"]
2134
  priority = p["priority"]
2135
+
2136
+ # Priority gating: "low" skips 3 of every 4 cycles to conserve RPM
2137
  if priority == "low" and (loop_counter % 4) != 0:
2138
+ log_activity(f"[Eternity Loop] Skipping low-priority '{name}' this cycle ({loop_counter})")
2139
  continue
2140
+
2141
  now = datetime.now(timezone.utc)
2142
  if current_mode == "build" and now >= deadline:
2143
  await update_db_mode(name, "eternity")
2144
  current_mode = "eternity"
2145
+ log_activity(f"[Eternity Loop] Project '{name}' deadline passed transitioned to Eternity R&D Mode.")
2146
+
2147
  if current_mode == "build":
2148
  await execute_build_cycle(name, goal)
2149
  else:
 
2150
  last_run = last_run_times.get(name, 0.0)
2151
  now_ts = time.time()
2152
  interval = int(os.environ.get("ETERNITY_LOOP_INTERVAL", "3600"))
2153
  if now_ts - last_run < interval:
2154
+ remaining_min = int((interval - (now_ts - last_run)) / 60)
2155
+ log_activity(f"[Eternity Loop] Project '{name}' sleeping — next R&D in {remaining_min}m")
2156
  continue
2157
  last_run_times[name] = now_ts
 
2158
  await execute_eternity_cycle(name, goal)
2159
+
2160
+ # Base check interval: 5 minutes
2161
  await asyncio.sleep(300)
2162
+
2163
  except Exception as e:
2164
+ log_activity(f"[Eternity Loop Error] Unhandled exception: {e}")
2165
  await asyncio.sleep(60)
2166
 
2167
 
 
2170
  return HTMLResponse(content=DASHBOARD_HTML)
2171
 
2172
 
2173
+ # ---------------------------------------------------------------------------
2174
+ # Vault Push Endpoint (Space 5 co-hosted here on Space 3)
2175
+ # Space 2 (Cerebrum) calls this after every successful build/eternity cycle.
2176
+ # It zips /tmp/workspace and logs the backup event.
2177
+ # A real git push would require SSH credentials injected via HF secrets.
2178
+ # ---------------------------------------------------------------------------
2179
+ class VaultPushRequest(BaseModel):
2180
+ project_name: Optional[str] = "unknown"
2181
+ summary: Optional[str] = ""
2182
+
2183
+ @app.post("/api/vault/push")
2184
+ async def vault_push(req: VaultPushRequest, authorization: str = Header(None)):
2185
+ auth(authorization)
2186
+ workspace = "/tmp/workspace"
2187
+ archive = "/tmp/vault_snapshot.zip"
2188
+ try:
2189
+ if os.path.exists(workspace) and os.listdir(workspace):
2190
+ proc = await asyncio.create_subprocess_exec(
2191
+ "zip", "-r", archive, workspace,
2192
+ stdout=asyncio.subprocess.DEVNULL,
2193
+ stderr=asyncio.subprocess.DEVNULL
2194
+ )
2195
+ await asyncio.wait_for(proc.communicate(), timeout=30)
2196
+ size_kb = int(os.path.getsize(archive) / 1024) if os.path.exists(archive) else 0
2197
+ log_activity(f"[Vault] Snapshot created for '{req.project_name}': {size_kb}KB | {req.summary[:80]}")
2198
+ return {"status": "success", "archive_kb": size_kb, "project_name": req.project_name}
2199
+ else:
2200
+ log_activity(f"[Vault] Workspace empty — no snapshot for '{req.project_name}'")
2201
+ return {"status": "skipped", "reason": "workspace empty"}
2202
+ except Exception as e:
2203
+ log_activity(f"[Vault Error] Snapshot failed for '{req.project_name}': {e}")
2204
+ return {"status": "error", "error": str(e)}
2205
+
2206
+
2207
+
2208
+
2209
  @app.get("/api/logs")
2210
  async def get_logs():
2211
  return list(activity_logs)
requirements.txt CHANGED
@@ -4,4 +4,5 @@ openai==1.86.0
4
  asyncpg==0.30.0
5
  anyio==4.9.0
6
  psutil==5.9.8
 
7
 
 
4
  asyncpg==0.30.0
5
  anyio==4.9.0
6
  psutil==5.9.8
7
+ httpx==0.27.2
8