77import logging
88import subprocess
99import sys
10+ from collections .abc import Sequence
1011from dataclasses import asdict , dataclass
1112from pathlib import Path
12- from typing import Any , Sequence
13-
14- from .objective_graph import DEFAULT_TASK_PREFIX , build_bundle_task_payloads , repo_relative_path , safe_bundle_key , utc_now
13+ from typing import Any
1514
15+ from .lease_coordination import LeaseCoordinator , LeaseError
16+ from .objective_graph import (
17+ DEFAULT_TASK_PREFIX ,
18+ build_bundle_task_payloads ,
19+ repo_relative_path ,
20+ safe_bundle_key ,
21+ utc_now ,
22+ )
1623
1724logger = logging .getLogger (__name__ )
1825
@@ -32,6 +39,10 @@ class BundleLaneSpec:
3239 command : list [str ]
3340 log_path : Path
3441 source_todo : str = ""
42+ task_cid : str = ""
43+ goal_cid : str = ""
44+ subgoal_cid : str = ""
45+ queue_payload : dict [str , Any ] | None = None
3546
3647 def to_dict (self , * , repo_root : Path | None = None ) -> dict [str , Any ]:
3748 payload = asdict (self )
@@ -137,6 +148,7 @@ def plan_bundle_lanes(
137148 for item in payload .get ("tasks" , [])
138149 if isinstance (item , dict ) and item .get ("task_id" )
139150 ]
151+ profile_g = payload .get ("profile_g" ) if isinstance (payload .get ("profile_g" ), dict ) else {}
140152 command = implementation_supervisor_command (
141153 todo_path = todo_path ,
142154 state_dir = state_dir ,
@@ -165,6 +177,10 @@ def plan_bundle_lanes(
165177 command = command ,
166178 log_path = log_path ,
167179 source_todo = str (payload .get ("source_todo" ) or "" ),
180+ task_cid = str (profile_g .get ("task_cid" ) or "" ),
181+ goal_cid = str (profile_g .get ("goal_cid" ) or "" ),
182+ subgoal_cid = str (profile_g .get ("subgoal_cid" ) or "" ),
183+ queue_payload = dict (payload ),
168184 )
169185 )
170186 return lanes
@@ -174,52 +190,77 @@ def launch_bundle_lanes(
174190 lanes : Sequence [BundleLaneSpec ],
175191 * ,
176192 repo_root : Path ,
193+ coordination_path : Path | None = None ,
194+ claimant_did : str = "did:web:ipfs-accelerate.local" ,
195+ lease_ms : int = 60_000 ,
196+ heartbeat_interval : float = 5.0 ,
197+ capacity_millionths : int = 1_000_000 ,
177198) -> list [dict [str , Any ]]:
178- """Launch lane supervisors in detached process sessions ."""
199+ """Claim and launch lane supervisors under accepted, fenced leases ."""
179200
180201 results : list [dict [str , Any ]] = []
181- for lane in lanes :
182- lane .state_dir .mkdir (parents = True , exist_ok = True )
183- lane .worktree_root .mkdir (parents = True , exist_ok = True )
184- lane .log_path .parent .mkdir (parents = True , exist_ok = True )
185- handle = lane .log_path .open ("ab" )
186- try :
187- process = subprocess .Popen (
188- lane .command ,
189- cwd = repo_root ,
190- stdin = subprocess .DEVNULL ,
191- stdout = handle ,
192- stderr = subprocess .STDOUT ,
193- start_new_session = True ,
202+ path = coordination_path or default_state_root (repo_root ) / "coordination.sqlite3"
203+ with LeaseCoordinator (path ) as coordinator :
204+ for lane in lanes :
205+ if not lane .queue_payload :
206+ results .append ({"bundle_key" : lane .bundle_key , "accepted" : False , "error" : "missing queue payload" })
207+ continue
208+ adapted = coordinator .register_bundle (lane .queue_payload )
209+ try :
210+ grant = coordinator .claim (adapted ["task_cid" ], claimant_did , requested_lease_ms = lease_ms )
211+ except LeaseError as exc :
212+ results .append ({"bundle_key" : lane .bundle_key , "accepted" : False , "error" : str (exc ), "code" : exc .code })
213+ continue
214+ lane .state_dir .mkdir (parents = True , exist_ok = True )
215+ lane .worktree_root .mkdir (parents = True , exist_ok = True )
216+ lane .log_path .parent .mkdir (parents = True , exist_ok = True )
217+ guarded_command = [
218+ sys .executable , "-m" , "ipfs_accelerate_py.agent_supervisor.leased_lane" ,
219+ "--coordination-path" , str (path ), "--grant-json" , json .dumps (grant .to_dict (), sort_keys = True ),
220+ "--lease-ms" , str (lease_ms ), "--heartbeat-interval" , str (heartbeat_interval ),
221+ "--capacity-millionths" , str (capacity_millionths ), "--" , * lane .command ,
222+ ]
223+ handle = lane .log_path .open ("ab" )
224+ try :
225+ try :
226+ process = subprocess .Popen (
227+ guarded_command ,
228+ cwd = repo_root ,
229+ stdin = subprocess .DEVNULL ,
230+ stdout = handle ,
231+ stderr = subprocess .STDOUT ,
232+ start_new_session = True ,
233+ )
234+ except Exception :
235+ coordinator .release (grant )
236+ raise
237+ finally :
238+ handle .close ()
239+ pid_path = lane .state_dir / f"{ lane .state_prefix } _bundle_supervisor.pid"
240+ pid_path .write_text (f"{ process .pid } \n " , encoding = "utf-8" )
241+ results .append (
242+ {
243+ "bundle_key" : lane .bundle_key , "accepted" : True , "pid" : process .pid ,
244+ "pid_path" : repo_relative_path (repo_root , pid_path ), "log_path" : repo_relative_path (repo_root , lane .log_path ),
245+ "command" : guarded_command , "lease" : grant .to_dict (),
246+ }
194247 )
195- finally :
196- handle .close ()
197- pid_path = lane .state_dir / f"{ lane .state_prefix } _bundle_supervisor.pid"
198- pid_path .write_text (f"{ process .pid } \n " , encoding = "utf-8" )
199- results .append (
200- {
201- "bundle_key" : lane .bundle_key ,
202- "pid" : process .pid ,
203- "pid_path" : repo_relative_path (repo_root , pid_path ),
204- "log_path" : repo_relative_path (repo_root , lane .log_path ),
205- "command" : lane .command ,
206- }
207- )
208248 return results
209249
210250
211251def check_lane_health (
212252 lanes : Sequence [BundleLaneSpec ],
213253 * ,
214254 repo_root : Path ,
255+ coordination_path : Path | None = None ,
256+ claimant_did : str = "did:web:ipfs-accelerate.local" ,
215257) -> list [dict [str , Any ]]:
216258 """Check health of all launched lanes and restart any dead ones.
217259
218260 Returns a list of health reports, one per lane. Dead lanes are
219261 automatically relaunched so the bundle supervisor can run indefinitely.
220262 """
221263 import os
222- import signal
223264
224265 reports : list [dict [str , Any ]] = []
225266 for lane in lanes :
@@ -243,23 +284,13 @@ def check_lane_health(
243284 if not report ["alive" ]:
244285 # Restart the dead lane
245286 try :
246- lane .log_path .parent .mkdir (parents = True , exist_ok = True )
247- handle = lane .log_path .open ("ab" )
248- try :
249- process = subprocess .Popen (
250- lane .command ,
251- cwd = repo_root ,
252- stdin = subprocess .DEVNULL ,
253- stdout = handle ,
254- stderr = subprocess .STDOUT ,
255- start_new_session = True ,
256- )
257- finally :
258- handle .close ()
259- pid_path .write_text (f"{ process .pid } \n " , encoding = "utf-8" )
260- report ["restarted" ] = True
261- report ["new_pid" ] = process .pid
262- logger .info ("Restarted dead lane %s with PID %d" , lane .bundle_key , process .pid )
287+ started = launch_bundle_lanes ([lane ], repo_root = repo_root , coordination_path = coordination_path , claimant_did = claimant_did )
288+ if started and started [0 ].get ("accepted" ):
289+ report ["restarted" ] = True
290+ report ["new_pid" ] = started [0 ]["pid" ]
291+ logger .info ("Restarted dead lane %s with PID %d" , lane .bundle_key , started [0 ]["pid" ])
292+ else :
293+ report ["restart_error" ] = str (started [0 ].get ("error" ) if started else "lease not accepted" )
263294 except OSError as exc :
264295 report ["restart_error" ] = str (exc )
265296 logger .error ("Failed to restart lane %s: %s" , lane .bundle_key , exc )
@@ -317,6 +348,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
317348 parser .add_argument ("--implementation-timeout" , type = float , default = 1800.0 )
318349 parser .add_argument ("--implementation-command" , default = "" )
319350 parser .add_argument ("--log-level" , default = "INFO" , choices = ["DEBUG" , "INFO" , "WARNING" , "ERROR" ])
351+ parser .add_argument ("--coordination-path" , type = Path , default = None )
352+ parser .add_argument ("--claimant-did" , default = "did:web:ipfs-accelerate.local" )
353+ parser .add_argument ("--lease-ms" , type = int , default = 60_000 )
354+ parser .add_argument ("--heartbeat-interval" , type = float , default = 5.0 )
355+ parser .add_argument ("--capacity-millionths" , type = int , default = 1_000_000 )
320356 return parser
321357
322358
@@ -344,7 +380,15 @@ def run_bundle_supervisor(args: argparse.Namespace) -> dict[str, Any]:
344380 log_level = args .log_level ,
345381 max_lanes = args .max_lanes ,
346382 )
347- started = launch_bundle_lanes (lanes , repo_root = repo_root ) if args .start else []
383+ started = launch_bundle_lanes (
384+ lanes ,
385+ repo_root = repo_root ,
386+ coordination_path = getattr (args , "coordination_path" , None ),
387+ claimant_did = getattr (args , "claimant_did" , "did:web:ipfs-accelerate.local" ),
388+ lease_ms = getattr (args , "lease_ms" , 60_000 ),
389+ heartbeat_interval = getattr (args , "heartbeat_interval" , 5.0 ),
390+ capacity_millionths = getattr (args , "capacity_millionths" , 1_000_000 ),
391+ ) if args .start else []
348392 payload = write_bundle_lane_manifest (
349393 manifest_path = manifest_path ,
350394 repo_root = repo_root ,
0 commit comments