Get features with satisfied dependencies, not already running. Args: feature_dicts: Pre-fetched list of feature dicts. If None, queries the database. scheduling_scores: Pre-computed scheduling scores. If None, computed from feature_dicts.
(
self,
feature_dicts: list[dict] | None = None,
scheduling_scores: dict[int, float] | None = None,
)
| 507 | return resumable |
| 508 | |
| 509 | def get_ready_features( |
| 510 | self, |
| 511 | feature_dicts: list[dict] | None = None, |
| 512 | scheduling_scores: dict[int, float] | None = None, |
| 513 | ) -> list[dict]: |
| 514 | """Get features with satisfied dependencies, not already running. |
| 515 | |
| 516 | Args: |
| 517 | feature_dicts: Pre-fetched list of feature dicts. If None, queries the database. |
| 518 | scheduling_scores: Pre-computed scheduling scores. If None, computed from feature_dicts. |
| 519 | """ |
| 520 | if feature_dicts is None: |
| 521 | session = self.get_session() |
| 522 | try: |
| 523 | session.expire_all() |
| 524 | all_features = session.query(Feature).all() |
| 525 | feature_dicts = [f.to_dict() for f in all_features] |
| 526 | finally: |
| 527 | session.close() |
| 528 | |
| 529 | # Pre-compute passing_ids once to avoid O(n^2) in the loop |
| 530 | passing_ids = {fd["id"] for fd in feature_dicts if fd.get("passes")} |
| 531 | |
| 532 | # Snapshot running IDs once (include all batch feature IDs) |
| 533 | with self._lock: |
| 534 | running_ids = set(self.running_coding_agents.keys()) |
| 535 | for batch_ids in self._batch_features.values(): |
| 536 | running_ids.update(batch_ids) |
| 537 | |
| 538 | ready = [] |
| 539 | skipped_reasons = {"passes": 0, "in_progress": 0, "running": 0, "failed": 0, "deps": 0} |
| 540 | for fd in feature_dicts: |
| 541 | if fd.get("passes"): |
| 542 | skipped_reasons["passes"] += 1 |
| 543 | continue |
| 544 | if fd.get("in_progress"): |
| 545 | skipped_reasons["in_progress"] += 1 |
| 546 | continue |
| 547 | # Skip if already running in this orchestrator |
| 548 | if fd["id"] in running_ids: |
| 549 | skipped_reasons["running"] += 1 |
| 550 | continue |
| 551 | # Skip if feature has failed too many times |
| 552 | if self._failure_counts.get(fd["id"], 0) >= MAX_FEATURE_RETRIES: |
| 553 | skipped_reasons["failed"] += 1 |
| 554 | continue |
| 555 | # Check dependencies (pass pre-computed passing_ids) |
| 556 | if are_dependencies_satisfied(fd, feature_dicts, passing_ids): |
| 557 | ready.append(fd) |
| 558 | else: |
| 559 | skipped_reasons["deps"] += 1 |
| 560 | |
| 561 | # Sort by scheduling score (higher = first), then priority, then id |
| 562 | if scheduling_scores is None: |
| 563 | scheduling_scores = compute_scheduling_scores(feature_dicts) |
| 564 | ready.sort(key=lambda f: (-scheduling_scores.get(f["id"], 0), f["priority"], f["id"])) |
| 565 | |
| 566 | # Summary counts for logging |
no test coverage detected