Spaces:
Paused
Paused
Claude Code commited on
Commit ·
807ecaf
1
Parent(s): 39dcfb3
god: implement ACTIVE TASK LOCKING Protocol — Prevent Redundant Execution on Same Files
Browse files- Add file lock mechanism (_file_locks) to track which files are being modified
- Extract file targets from task descriptions using regex patterns
- Block tasks that conflict with existing file locks (different agent)
- Display active file locks to agents in turn messages
- Clear locks when tasks complete
- Forces agents into REVIEW mode when files are contested
- scripts/conversation-loop.py +103 -10
scripts/conversation-loop.py
CHANGED
|
@@ -2077,11 +2077,77 @@ _pending_task_timestamp = 0.0 # when was the task submitted?
|
|
| 2077 |
_pending_task_speaker = "" # who submitted it?
|
| 2078 |
_pending_task_desc = "" # what was the task?
|
| 2079 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2080 |
|
| 2081 |
def parse_and_execute_turn(raw_text, ctx):
|
| 2082 |
"""Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
|
| 2083 |
global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result, _discussion_loop_count
|
| 2084 |
-
global _pending_task_just_submitted, _pending_task_timestamp, _pending_task_speaker, _pending_task_desc
|
| 2085 |
results = []
|
| 2086 |
task_assigned = False
|
| 2087 |
|
|
@@ -2123,14 +2189,24 @@ def parse_and_execute_turn(raw_text, ctx):
|
|
| 2123 |
last_rebuild_trigger_at = 0
|
| 2124 |
|
| 2125 |
if not results: # not blocked
|
| 2126 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
| 2132 |
-
|
| 2133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2134 |
|
| 2135 |
# 3. Handle [ACTION: restart] (escape hatch)
|
| 2136 |
if re.search(r'\[ACTION:\s*restart\]', raw_text):
|
|
@@ -2212,7 +2288,7 @@ def build_turn_message(speaker, other, ctx):
|
|
| 2212 |
(SOUL.md, IDENTITY.md, workspace/memory/). This message provides only
|
| 2213 |
context and turn instructions.
|
| 2214 |
"""
|
| 2215 |
-
global _pending_task_just_submitted, _pending_task_timestamp, _pending_task_speaker, _pending_task_desc, _discussion_loop_count, _sanity_check_mode, _sanity_check_required
|
| 2216 |
parts = []
|
| 2217 |
|
| 2218 |
# Brief role context (supplements agent's SOUL.md until it's fully configured)
|
|
@@ -2259,6 +2335,17 @@ def build_turn_message(speaker, other, ctx):
|
|
| 2259 |
# Claude Code live status (async)
|
| 2260 |
parts.append(f"\n=== CLAUDE CODE STATUS ===\n{cc_get_live_status()}")
|
| 2261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2262 |
# Auto-gathered context
|
| 2263 |
parts.append(f"\n=== {CHILD_NAME}'S CURRENT STATE ===")
|
| 2264 |
parts.append(format_context(ctx))
|
|
@@ -2749,15 +2836,21 @@ def do_turn(speaker, other, space_url):
|
|
| 2749 |
_context_cache.clear()
|
| 2750 |
# Clear pending task flag since CC finished
|
| 2751 |
_pending_task_just_submitted = False
|
|
|
|
|
|
|
| 2752 |
# CRITICAL FIX: Also clear pending task flag when CC finishes, regardless of speaker
|
| 2753 |
# This fixes the race condition where Adam's turn comes before Eve's after CC finishes
|
| 2754 |
# ALSO: Clear when CC is not running (handles auto-termination where result is cleared)
|
| 2755 |
elif cc_just_finished and _pending_task_just_submitted:
|
| 2756 |
_pending_task_just_submitted = False
|
|
|
|
|
|
|
| 2757 |
elif not cc_status["running"] and _pending_task_just_submitted:
|
| 2758 |
# CC finished but result was cleared (e.g., auto-termination for handoff)
|
| 2759 |
# Clear the pending flag so agents can submit new tasks
|
| 2760 |
_pending_task_just_submitted = False
|
|
|
|
|
|
|
| 2761 |
|
| 2762 |
# Add to history with timestamp (text stays CLEAN for agent context)
|
| 2763 |
ts = datetime.datetime.utcnow().strftime("%H:%M")
|
|
|
|
| 2077 |
_pending_task_speaker = "" # who submitted it?
|
| 2078 |
_pending_task_desc = "" # what was the task?
|
| 2079 |
|
| 2080 |
+
# Active Task Locking (Mutex) — prevents redundant execution on same files
|
| 2081 |
+
_file_locks = {} # {file_path: {"agent": speaker, "timestamp": time.time(), "task": task_desc}}
|
| 2082 |
+
_LOCK_DURATION = 600 # seconds (10 minutes) - locks expire after this time
|
| 2083 |
+
|
| 2084 |
+
|
| 2085 |
+
def _extract_file_targets(task_desc):
|
| 2086 |
+
"""Extract file paths from a task description.
|
| 2087 |
+
Returns a set of file paths that are being modified.
|
| 2088 |
+
"""
|
| 2089 |
+
import re
|
| 2090 |
+
files = set()
|
| 2091 |
+
# Match common patterns: "app.py", "/path/to/file.py", "file in <path>", "modify <file>"
|
| 2092 |
+
# Direct file mentions
|
| 2093 |
+
files.update(re.findall(r'\b([\w/]+\.py)\b', task_desc))
|
| 2094 |
+
files.update(re.findall(r'\b([\w/]+\.md)\b', task_desc))
|
| 2095 |
+
files.update(re.findall(r'\b([\w/]+\.txt)\b', task_desc))
|
| 2096 |
+
files.update(re.findall(r'\b([\w/]+\.json)\b', task_desc))
|
| 2097 |
+
files.update(re.findall(r'\b([\w/]+\.yaml)\b', task_desc))
|
| 2098 |
+
files.update(re.findall(r'\b([\w/]+\.yml)\b', task_desc))
|
| 2099 |
+
# Path patterns
|
| 2100 |
+
files.update(re.findall(r'/tmp/[\w/]+', task_desc))
|
| 2101 |
+
files.update(re.findall(r'/app/[\w/]+', task_desc))
|
| 2102 |
+
return files
|
| 2103 |
+
|
| 2104 |
+
|
| 2105 |
+
def _check_file_lock_conflict(files, speaker):
|
| 2106 |
+
"""Check if any of the files are locked by another agent.
|
| 2107 |
+
Returns (has_conflict: bool, conflict_details: str)
|
| 2108 |
+
"""
|
| 2109 |
+
import time
|
| 2110 |
+
now = time.time()
|
| 2111 |
+
# Clean expired locks
|
| 2112 |
+
expired = [f for f, lock in _file_locks.items() if now - lock["timestamp"] > _LOCK_DURATION]
|
| 2113 |
+
for f in expired:
|
| 2114 |
+
del _file_locks[f]
|
| 2115 |
+
|
| 2116 |
+
conflicts = []
|
| 2117 |
+
for f in files:
|
| 2118 |
+
if f in _file_locks:
|
| 2119 |
+
lock = _file_locks[f]
|
| 2120 |
+
if lock["agent"] != speaker:
|
| 2121 |
+
elapsed = int(now - lock["timestamp"])
|
| 2122 |
+
conflicts.append(f"'{f}' (locked by {lock['agent']} {elapsed}s ago)")
|
| 2123 |
+
if conflicts:
|
| 2124 |
+
return True, f"Files {', '.join(conflicts)} are already being modified. Wait for the other agent to finish."
|
| 2125 |
+
return False, None
|
| 2126 |
+
|
| 2127 |
+
|
| 2128 |
+
def _acquire_file_locks(files, speaker, task_desc):
|
| 2129 |
+
"""Acquire locks for the given files."""
|
| 2130 |
+
import time
|
| 2131 |
+
for f in files:
|
| 2132 |
+
_file_locks[f] = {
|
| 2133 |
+
"agent": speaker,
|
| 2134 |
+
"timestamp": time.time(),
|
| 2135 |
+
"task": task_desc[:100]
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
|
| 2139 |
+
def _clear_file_locks(speaker):
|
| 2140 |
+
"""Clear all locks held by a specific agent (when their task completes)."""
|
| 2141 |
+
global _file_locks
|
| 2142 |
+
to_remove = [f for f, lock in _file_locks.items() if lock["agent"] == speaker]
|
| 2143 |
+
for f in to_remove:
|
| 2144 |
+
del _file_locks[f]
|
| 2145 |
+
|
| 2146 |
|
| 2147 |
def parse_and_execute_turn(raw_text, ctx):
|
| 2148 |
"""Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
|
| 2149 |
global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result, _discussion_loop_count
|
| 2150 |
+
global _pending_task_just_submitted, _pending_task_timestamp, _pending_task_speaker, _pending_task_desc, _file_locks
|
| 2151 |
results = []
|
| 2152 |
task_assigned = False
|
| 2153 |
|
|
|
|
| 2189 |
last_rebuild_trigger_at = 0
|
| 2190 |
|
| 2191 |
if not results: # not blocked
|
| 2192 |
+
# FILE LOCK CHECK: Prevent redundant execution on same files
|
| 2193 |
+
files = _extract_file_targets(task_desc)
|
| 2194 |
+
if files:
|
| 2195 |
+
has_conflict, conflict_msg = _check_file_lock_conflict(files, _current_speaker)
|
| 2196 |
+
if has_conflict:
|
| 2197 |
+
results.append({"action": "task", "result": f"BLOCKED: {conflict_msg} Switch to REVIEW mode or analyze a different subsystem."})
|
| 2198 |
+
if not results: # not blocked by file lock
|
| 2199 |
+
submit_result = cc_submit_task(task_desc, _current_speaker, ctx)
|
| 2200 |
+
results.append({"action": "claude_code", "result": submit_result})
|
| 2201 |
+
task_assigned = True # Only mark as assigned when actually submitted
|
| 2202 |
+
# Track the pending task so other agent knows about it
|
| 2203 |
+
_pending_task_just_submitted = True
|
| 2204 |
+
_pending_task_timestamp = time.time()
|
| 2205 |
+
_pending_task_speaker = _current_speaker
|
| 2206 |
+
_pending_task_desc = task_desc[:200]
|
| 2207 |
+
# Acquire file locks for this task
|
| 2208 |
+
if files:
|
| 2209 |
+
_acquire_file_locks(files, _current_speaker, task_desc)
|
| 2210 |
|
| 2211 |
# 3. Handle [ACTION: restart] (escape hatch)
|
| 2212 |
if re.search(r'\[ACTION:\s*restart\]', raw_text):
|
|
|
|
| 2288 |
(SOUL.md, IDENTITY.md, workspace/memory/). This message provides only
|
| 2289 |
context and turn instructions.
|
| 2290 |
"""
|
| 2291 |
+
global _pending_task_just_submitted, _pending_task_timestamp, _pending_task_speaker, _pending_task_desc, _discussion_loop_count, _sanity_check_mode, _sanity_check_required, _file_locks
|
| 2292 |
parts = []
|
| 2293 |
|
| 2294 |
# Brief role context (supplements agent's SOUL.md until it's fully configured)
|
|
|
|
| 2335 |
# Claude Code live status (async)
|
| 2336 |
parts.append(f"\n=== CLAUDE CODE STATUS ===\n{cc_get_live_status()}")
|
| 2337 |
|
| 2338 |
+
# Active Task Locking (Mutex) status — show which files are locked
|
| 2339 |
+
if _file_locks:
|
| 2340 |
+
import time
|
| 2341 |
+
now = time.time()
|
| 2342 |
+
lock_info = []
|
| 2343 |
+
for f, lock in _file_locks.items():
|
| 2344 |
+
elapsed = int(now - lock["timestamp"])
|
| 2345 |
+
lock_info.append(f" - {f} (locked by {lock['agent']}, {elapsed}s ago)")
|
| 2346 |
+
parts.append(f"\n=== ACTIVE FILE LOCKS ===\nFiles currently being modified:\n" + "\n".join(lock_info))
|
| 2347 |
+
parts.append(f"\nIMPORTANT: If your task targets these files, you must WAIT for {lock['agent']} to finish. Switch to REVIEW mode or analyze a different subsystem.")
|
| 2348 |
+
|
| 2349 |
# Auto-gathered context
|
| 2350 |
parts.append(f"\n=== {CHILD_NAME}'S CURRENT STATE ===")
|
| 2351 |
parts.append(format_context(ctx))
|
|
|
|
| 2836 |
_context_cache.clear()
|
| 2837 |
# Clear pending task flag since CC finished
|
| 2838 |
_pending_task_just_submitted = False
|
| 2839 |
+
# Clear file locks for the agent who just completed their task
|
| 2840 |
+
_clear_file_locks(_pending_task_speaker)
|
| 2841 |
# CRITICAL FIX: Also clear pending task flag when CC finishes, regardless of speaker
|
| 2842 |
# This fixes the race condition where Adam's turn comes before Eve's after CC finishes
|
| 2843 |
# ALSO: Clear when CC is not running (handles auto-termination where result is cleared)
|
| 2844 |
elif cc_just_finished and _pending_task_just_submitted:
|
| 2845 |
_pending_task_just_submitted = False
|
| 2846 |
+
# Clear file locks for the agent who just completed their task
|
| 2847 |
+
_clear_file_locks(_pending_task_speaker)
|
| 2848 |
elif not cc_status["running"] and _pending_task_just_submitted:
|
| 2849 |
# CC finished but result was cleared (e.g., auto-termination for handoff)
|
| 2850 |
# Clear the pending flag so agents can submit new tasks
|
| 2851 |
_pending_task_just_submitted = False
|
| 2852 |
+
# Clear file locks for the agent who just completed their task
|
| 2853 |
+
_clear_file_locks(_pending_task_speaker)
|
| 2854 |
|
| 2855 |
# Add to history with timestamp (text stays CLEAN for agent context)
|
| 2856 |
ts = datetime.datetime.utcnow().strftime("%H:%M")
|