Spaces:
Paused
perf: major efficiency overhaul for conversation-loop mechanism
Browse files1. Dynamic cooldown: auto-clears when build finishes instead of
fixed 10min wait. Polls HF API to detect BUILDINGβRUNNING/ERROR.
2. Smart wait during BUILDING: skips LLM API calls entirely when
Cain is building/restarting. Polls health every 20s instead,
saving API costs and avoiding wasted turns.
3. Reduced base cooldown from 600s to 360s (6 min).
4. Knowledge cache clearing: after write_file, the file is removed
from the "already read" cache so agents can re-read to verify.
5. Per-cycle file write dedup: blocks writing the same Space file
twice in one build cycle, preventing overwrites.
6. Cycle reset on RUNNING: write dedup set clears when Cain
successfully enters RUNNING state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/conversation-loop.py +70 -7
|
@@ -130,8 +130,28 @@ child_state = {
|
|
| 130 |
}
|
| 131 |
|
| 132 |
# Rebuild cooldown β prevent rapid write_file to Space that keeps resetting builds
|
| 133 |
-
REBUILD_COOLDOWN_SECS =
|
| 134 |
last_rebuild_trigger_at = 0 # timestamp of last write_file to space
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
def init_child_state():
|
|
@@ -217,6 +237,7 @@ def action_check_health():
|
|
| 217 |
child_state["state"] = data.get("state", "unknown")
|
| 218 |
child_state["detail"] = data.get("detail", "")
|
| 219 |
child_state["stage"] = "RUNNING"
|
|
|
|
| 220 |
return (f"{CHILD_NAME} is ALIVE! State: {child_state['state']}, "
|
| 221 |
f"Detail: {child_state['detail'] or 'healthy'}")
|
| 222 |
except:
|
|
@@ -287,7 +308,7 @@ def action_restart():
|
|
| 287 |
child_state["alive"] = False
|
| 288 |
child_state["stage"] = "RESTARTING"
|
| 289 |
last_rebuild_trigger_at = time.time()
|
| 290 |
-
return f"{CHILD_NAME} is restarting. Will take a few minutes.
|
| 291 |
except Exception as e:
|
| 292 |
return f"Restart failed: {e}"
|
| 293 |
|
|
@@ -354,7 +375,7 @@ def action_write_file(target, path, content):
|
|
| 354 |
rebuild_note = ""
|
| 355 |
if target == "space":
|
| 356 |
last_rebuild_trigger_at = time.time()
|
| 357 |
-
rebuild_note = " β οΈ This triggers a Space rebuild!
|
| 358 |
return f"β Wrote {len(content)} bytes to {CHILD_NAME}'s {target}:{path}{rebuild_note}"
|
| 359 |
except Exception as e:
|
| 360 |
return f"Error writing {target}:{path}: {e}"
|
|
@@ -440,18 +461,27 @@ def parse_and_execute_actions(raw_text):
|
|
| 440 |
if write_match:
|
| 441 |
target, path, content = write_match.group(1), write_match.group(2).strip(), write_match.group(3).strip()
|
| 442 |
key = f"write_file:{target}:{path}"
|
|
|
|
| 443 |
if key not in executed:
|
| 444 |
executed.add(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
# Guard: block write_file during BUILDING/APP_STARTING/RESTARTING
|
| 446 |
-
|
| 447 |
result = (f"β BLOCKED: Cain is currently {child_state['stage']}. "
|
| 448 |
"Writing to Space during build RESETS the entire build from scratch. "
|
| 449 |
"Wait for it to finish, then try again.")
|
| 450 |
results.append({"action": key, "result": result})
|
| 451 |
print(f"[BLOCKED] {key} β Cain is {child_state['stage']}")
|
| 452 |
-
# Guard: rebuild cooldown
|
| 453 |
elif target == "space" and last_rebuild_trigger_at > 0:
|
| 454 |
-
|
|
|
|
| 455 |
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 456 |
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 457 |
result = (f"β BLOCKED: Rebuild cooldown active ({remaining}s remaining). "
|
|
@@ -462,10 +492,16 @@ def parse_and_execute_actions(raw_text):
|
|
| 462 |
result = action_write_file(target, path, content)
|
| 463 |
results.append({"action": key, "result": result})
|
| 464 |
print(f"[ACTION] {key} β {result[:100]}")
|
|
|
|
|
|
|
|
|
|
| 465 |
else:
|
| 466 |
result = action_write_file(target, path, content)
|
| 467 |
results.append({"action": key, "result": result})
|
| 468 |
print(f"[ACTION] {key} β {result[:100]}")
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
# 2. Handle all [ACTION/Action/ζδ½/ε¨δ½: ...] tags β case-insensitive, multilingual
|
| 471 |
for match in re.finditer(r'\[(?:ACTION|Action|action|ζδ½|ε¨δ½)\s*[:οΌ]\s*([^\]]+)\]', raw_text):
|
|
@@ -501,7 +537,8 @@ def parse_and_execute_actions(raw_text):
|
|
| 501 |
|
| 502 |
# Rebuild cooldown β prevent writing to Space repo too soon after last rebuild trigger
|
| 503 |
if name in ("write_file", "set_env", "set_secret", "restart") and last_rebuild_trigger_at > 0:
|
| 504 |
-
|
|
|
|
| 505 |
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 506 |
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 507 |
result = (f"β BLOCKED: Rebuild cooldown active β last Space change was {int(elapsed)}s ago. "
|
|
@@ -1114,8 +1151,34 @@ if reply:
|
|
| 1114 |
time.sleep(15)
|
| 1115 |
|
| 1116 |
while True:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1118 |
time.sleep(15)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
do_turn("Adam", "Eve", ADAM_SPACE)
|
| 1120 |
|
| 1121 |
if len(history) > MAX_HISTORY:
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
# Rebuild cooldown β prevent rapid write_file to Space that keeps resetting builds
|
| 133 |
+
REBUILD_COOLDOWN_SECS = 360 # 6 minutes (builds typically finish in 3-5 min)
|
| 134 |
last_rebuild_trigger_at = 0 # timestamp of last write_file to space
|
| 135 |
+
files_written_this_cycle = set() # track files written since last RUNNING state
|
| 136 |
+
|
| 137 |
+
def check_and_clear_cooldown():
|
| 138 |
+
"""Auto-clear cooldown if Cain has finished building (dynamic cooldown)."""
|
| 139 |
+
global last_rebuild_trigger_at
|
| 140 |
+
if last_rebuild_trigger_at == 0:
|
| 141 |
+
return
|
| 142 |
+
elapsed = time.time() - last_rebuild_trigger_at
|
| 143 |
+
if elapsed < 60: # always wait at least 60s
|
| 144 |
+
return
|
| 145 |
+
try:
|
| 146 |
+
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 147 |
+
stage = info.runtime.stage if info.runtime else "unknown"
|
| 148 |
+
if stage in ("RUNNING", "RUNTIME_ERROR", "BUILD_ERROR"):
|
| 149 |
+
print(f"[COOLDOWN] Build finished (stage={stage}), clearing cooldown early ({int(elapsed)}s elapsed)")
|
| 150 |
+
last_rebuild_trigger_at = 0
|
| 151 |
+
child_state["stage"] = stage
|
| 152 |
+
child_state["alive"] = (stage == "RUNNING")
|
| 153 |
+
except:
|
| 154 |
+
pass
|
| 155 |
|
| 156 |
|
| 157 |
def init_child_state():
|
|
|
|
| 237 |
child_state["state"] = data.get("state", "unknown")
|
| 238 |
child_state["detail"] = data.get("detail", "")
|
| 239 |
child_state["stage"] = "RUNNING"
|
| 240 |
+
files_written_this_cycle.clear() # reset write dedup on successful run
|
| 241 |
return (f"{CHILD_NAME} is ALIVE! State: {child_state['state']}, "
|
| 242 |
f"Detail: {child_state['detail'] or 'healthy'}")
|
| 243 |
except:
|
|
|
|
| 308 |
child_state["alive"] = False
|
| 309 |
child_state["stage"] = "RESTARTING"
|
| 310 |
last_rebuild_trigger_at = time.time()
|
| 311 |
+
return f"{CHILD_NAME} is restarting. Will take a few minutes. Cooldown starts now (clears automatically when build finishes)."
|
| 312 |
except Exception as e:
|
| 313 |
return f"Restart failed: {e}"
|
| 314 |
|
|
|
|
| 375 |
rebuild_note = ""
|
| 376 |
if target == "space":
|
| 377 |
last_rebuild_trigger_at = time.time()
|
| 378 |
+
rebuild_note = " β οΈ This triggers a Space rebuild! Cooldown starts now (auto-clears when build finishes)."
|
| 379 |
return f"β Wrote {len(content)} bytes to {CHILD_NAME}'s {target}:{path}{rebuild_note}"
|
| 380 |
except Exception as e:
|
| 381 |
return f"Error writing {target}:{path}: {e}"
|
|
|
|
| 461 |
if write_match:
|
| 462 |
target, path, content = write_match.group(1), write_match.group(2).strip(), write_match.group(3).strip()
|
| 463 |
key = f"write_file:{target}:{path}"
|
| 464 |
+
file_id = f"{target}:{path}"
|
| 465 |
if key not in executed:
|
| 466 |
executed.add(key)
|
| 467 |
+
# Guard: duplicate write to same file this cycle
|
| 468 |
+
if target == "space" and file_id in files_written_this_cycle:
|
| 469 |
+
result = (f"β BLOCKED: {path} was already written this cycle. "
|
| 470 |
+
"Wait for the build to finish and verify before writing again. "
|
| 471 |
+
"Writing the same file twice wastes a rebuild cycle.")
|
| 472 |
+
results.append({"action": key, "result": result})
|
| 473 |
+
print(f"[BLOCKED] {key} β duplicate write this cycle")
|
| 474 |
# Guard: block write_file during BUILDING/APP_STARTING/RESTARTING
|
| 475 |
+
elif target == "space" and child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 476 |
result = (f"β BLOCKED: Cain is currently {child_state['stage']}. "
|
| 477 |
"Writing to Space during build RESETS the entire build from scratch. "
|
| 478 |
"Wait for it to finish, then try again.")
|
| 479 |
results.append({"action": key, "result": result})
|
| 480 |
print(f"[BLOCKED] {key} β Cain is {child_state['stage']}")
|
| 481 |
+
# Guard: rebuild cooldown (check dynamically first)
|
| 482 |
elif target == "space" and last_rebuild_trigger_at > 0:
|
| 483 |
+
check_and_clear_cooldown() # may clear cooldown early if build done
|
| 484 |
+
elapsed = time.time() - last_rebuild_trigger_at if last_rebuild_trigger_at > 0 else 9999
|
| 485 |
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 486 |
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 487 |
result = (f"β BLOCKED: Rebuild cooldown active ({remaining}s remaining). "
|
|
|
|
| 492 |
result = action_write_file(target, path, content)
|
| 493 |
results.append({"action": key, "result": result})
|
| 494 |
print(f"[ACTION] {key} β {result[:100]}")
|
| 495 |
+
files_written_this_cycle.add(file_id)
|
| 496 |
+
# Clear knowledge cache so agents can re-read the file they just wrote
|
| 497 |
+
knowledge["files_read"].discard(file_id)
|
| 498 |
else:
|
| 499 |
result = action_write_file(target, path, content)
|
| 500 |
results.append({"action": key, "result": result})
|
| 501 |
print(f"[ACTION] {key} β {result[:100]}")
|
| 502 |
+
if target == "space":
|
| 503 |
+
files_written_this_cycle.add(file_id)
|
| 504 |
+
knowledge["files_read"].discard(file_id)
|
| 505 |
|
| 506 |
# 2. Handle all [ACTION/Action/ζδ½/ε¨δ½: ...] tags β case-insensitive, multilingual
|
| 507 |
for match in re.finditer(r'\[(?:ACTION|Action|action|ζδ½|ε¨δ½)\s*[:οΌ]\s*([^\]]+)\]', raw_text):
|
|
|
|
| 537 |
|
| 538 |
# Rebuild cooldown β prevent writing to Space repo too soon after last rebuild trigger
|
| 539 |
if name in ("write_file", "set_env", "set_secret", "restart") and last_rebuild_trigger_at > 0:
|
| 540 |
+
check_and_clear_cooldown() # may clear cooldown early if build done
|
| 541 |
+
elapsed = time.time() - last_rebuild_trigger_at if last_rebuild_trigger_at > 0 else 9999
|
| 542 |
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 543 |
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 544 |
result = (f"β BLOCKED: Rebuild cooldown active β last Space change was {int(elapsed)}s ago. "
|
|
|
|
| 1151 |
time.sleep(15)
|
| 1152 |
|
| 1153 |
while True:
|
| 1154 |
+
# Smart wait: if Cain is BUILDING/APP_STARTING, skip LLM calls and just poll
|
| 1155 |
+
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 1156 |
+
print(f"[WAIT] Cain is {child_state['stage']} β polling health instead of LLM call...")
|
| 1157 |
+
check_and_clear_cooldown()
|
| 1158 |
+
# Quick health check to update stage
|
| 1159 |
+
try:
|
| 1160 |
+
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 1161 |
+
new_stage = info.runtime.stage if info.runtime else "unknown"
|
| 1162 |
+
if new_stage != child_state["stage"]:
|
| 1163 |
+
print(f"[WAIT] Stage changed: {child_state['stage']} β {new_stage}")
|
| 1164 |
+
child_state["stage"] = new_stage
|
| 1165 |
+
child_state["alive"] = (new_stage == "RUNNING")
|
| 1166 |
+
else:
|
| 1167 |
+
print(f"[WAIT] Still {new_stage}... waiting 20s")
|
| 1168 |
+
except Exception as e:
|
| 1169 |
+
print(f"[WAIT] Health check error: {e}")
|
| 1170 |
+
time.sleep(20)
|
| 1171 |
+
continue
|
| 1172 |
+
|
| 1173 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1174 |
time.sleep(15)
|
| 1175 |
+
|
| 1176 |
+
# Check if we just triggered a build β skip Adam's turn if so
|
| 1177 |
+
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 1178 |
+
print(f"[SKIP] Cain entered {child_state['stage']} β skipping Adam's turn to avoid wasted LLM call")
|
| 1179 |
+
time.sleep(10)
|
| 1180 |
+
continue
|
| 1181 |
+
|
| 1182 |
do_turn("Adam", "Eve", ADAM_SPACE)
|
| 1183 |
|
| 1184 |
if len(history) > MAX_HISTORY:
|