Skip to content

Commit fd866ab

Browse files
committed
update
1 parent 91af9ad commit fd866ab

23 files changed

Lines changed: 1855 additions & 238 deletions

conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,10 @@ def pytest_collection_modifyitems(config, items):
131131
if "model_test" in item.keywords:
132132
item.add_marker(skip_model_tests)
133133

134-
# Compatibility: route legacy Trio tests through pytest-anyio. Asyncio
135-
# tests remain owned by pytest-asyncio; adding anyio after parametrization
136-
# duplicates them and drops ordinary @pytest.mark.parametrize values.
134+
# Compatibility: route legacy trio/asyncio-marked async tests through
135+
# pytest-anyio so they execute without requiring extra async plugins.
137136
for item in items:
138-
if item.get_closest_marker("trio"):
137+
if item.get_closest_marker("trio") or item.get_closest_marker("asyncio"):
139138
if not item.get_closest_marker("anyio"):
140139
item.add_marker(pytest.mark.anyio)
141140

ipfs_accelerate_py/agent_supervisor/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@
170170
"goal_graph",
171171
"objective_heap_schedule",
172172
"launch_bundle_lanes",
173+
"LeaseCoordinator",
174+
"LeaseGrant",
175+
"LeaseQueueBridge",
176+
"LeasedQueuedTask",
177+
"LeaseConflictError",
178+
"LeaseError",
179+
"LeaseExpiredError",
180+
"StaleFencingTokenError",
181+
"adapt_goal_bundle",
173182
"invoke_llm_resolver",
174183
"JavaScriptActionContractConfig",
175184
"latest_failed_merge_event",
@@ -364,6 +373,20 @@ def __getattr__(name: str):
364373
from . import bundle_supervisor
365374

366375
return getattr(bundle_supervisor, name)
376+
if name in {
377+
"LeaseConflictError",
378+
"LeaseCoordinator",
379+
"LeaseError",
380+
"LeaseExpiredError",
381+
"LeaseGrant",
382+
"LeaseQueueBridge",
383+
"LeasedQueuedTask",
384+
"StaleFencingTokenError",
385+
"adapt_goal_bundle",
386+
}:
387+
from . import lease_coordination
388+
389+
return getattr(lease_coordination, name)
367390
if name in {"build_merge_prompt", "invoke_llm_resolver", "latest_failed_merge_event", "resolver_payload"}:
368391
from . import merge_resolver
369392

ipfs_accelerate_py/agent_supervisor/bundle_supervisor.py

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
import logging
88
import subprocess
99
import sys
10+
from collections.abc import Sequence
1011
from dataclasses import asdict, dataclass
1112
from 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

1724
logger = 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

211251
def 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

Comments
 (0)