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

Files changed (1) hide show
  1. 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
- submit_result = cc_submit_task(task_desc, _current_speaker, ctx)
2127
- results.append({"action": "claude_code", "result": submit_result})
2128
- task_assigned = True # Only mark as assigned when actually submitted
2129
- # Track the pending task so other agent knows about it
2130
- _pending_task_just_submitted = True
2131
- _pending_task_timestamp = time.time()
2132
- _pending_task_speaker = _current_speaker
2133
- _pending_task_desc = task_desc[:200]
 
 
 
 
 
 
 
 
 
 
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")