tenseleyflow/loader / a53d8d8

Browse files

Extend multi-file retry budget

Authored by espadonne
SHA
a53d8d87392f3609cce9b492c69548ccae3b4929
Parents
a41b947
Tree
50a2684

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 21 1
M tests/test_repair.py 55 0
src/loader/runtime/repair.pymodified
@@ -28,6 +28,7 @@ _SPECIAL_DOD_ITEMS = {
2828
     "Collect verification evidence",
2929
 }
3030
 _LATE_STAGE_EMPTY_RETRY_EXTRA = 2
31
+_MULTI_FILE_OUTPUT_EMPTY_RETRY_EXTRA = 2
3132
 _WORKING_NOTE_TOOL_NAMES = (
3233
     "notepad_write_working",
3334
     "notepad_append",
@@ -419,7 +420,10 @@ class ResponseRepairer:
419420
         if completed_artifacts >= 3 and missing_artifacts > 0:
420421
             return base_max_empty_retries + _LATE_STAGE_EMPTY_RETRY_EXTRA
421422
         if self._has_concrete_next_output_step(dod):
422
-            return base_max_empty_retries + _LATE_STAGE_EMPTY_RETRY_EXTRA
423
+            extra_retries = _LATE_STAGE_EMPTY_RETRY_EXTRA
424
+            if self._has_confirmed_output_file_progress(dod):
425
+                extra_retries += _MULTI_FILE_OUTPUT_EMPTY_RETRY_EXTRA
426
+            return base_max_empty_retries + extra_retries
423427
         return base_max_empty_retries
424428
 
425429
     def _should_compact_empty_retry_message(self, dod: DefinitionOfDone) -> bool:
@@ -483,6 +487,22 @@ class ResponseRepairer:
483487
         )
484488
         return next_output_file is not None
485489
 
490
+    def _has_confirmed_output_file_progress(self, dod: DefinitionOfDone) -> bool:
491
+        return any(
492
+            not expect_directory
493
+            and planned_artifact_target_satisfied(
494
+                dod,
495
+                target=target,
496
+                expect_directory=False,
497
+                project_root=self.context.project_root,
498
+            )
499
+            for target, expect_directory in collect_planned_artifact_targets(
500
+                dod,
501
+                project_root=self.context.project_root,
502
+                max_paths=12,
503
+            )
504
+        )
505
+
486506
     def _planned_artifact_progress_lines(self, dod: DefinitionOfDone) -> list[str]:
487507
         targets = collect_planned_artifact_targets(
488508
             dod,
tests/test_repair.pymodified
@@ -497,6 +497,61 @@ def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
497497
     )
498498
 
499499
 
500
+def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
501
+    temp_dir: Path,
502
+) -> None:
503
+    context = build_context(
504
+        temp_dir=temp_dir,
505
+        use_react=False,
506
+    )
507
+    repairer = ResponseRepairer(context)
508
+
509
+    guide_root = temp_dir / "guides" / "nginx"
510
+    chapters = guide_root / "chapters"
511
+    guide_root.mkdir(parents=True)
512
+    chapters.mkdir()
513
+    index_path = guide_root / "index.html"
514
+    index_path.write_text("<html></html>\n")
515
+
516
+    implementation_plan = temp_dir / "implementation.md"
517
+    implementation_plan.write_text(
518
+        "\n".join(
519
+            [
520
+                "# Implementation Plan",
521
+                "",
522
+                "## File Changes",
523
+                f"- `{chapters}/`",
524
+                f"- `{index_path}`",
525
+                "",
526
+            ]
527
+        )
528
+    )
529
+
530
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
531
+    dod.implementation_plan = str(implementation_plan)
532
+    dod.touched_files.append(str(index_path))
533
+    dod.completed_items.extend(
534
+        [
535
+            "Create the new nginx guide directory structure",
536
+            "Develop the main index.html file with proper structure",
537
+        ]
538
+    )
539
+    dod.pending_items.append("Create 01-introduction.html")
540
+
541
+    decision = repairer.handle_empty_response(
542
+        task="Create a multi-file nginx guide.",
543
+        original_task=None,
544
+        empty_retry_count=5,
545
+        max_empty_retries=2,
546
+        dod=dod,
547
+    )
548
+
549
+    assert decision.should_continue is True
550
+    assert decision.retry_message is not None
551
+    assert "retry 5/6" in decision.retry_message
552
+    assert "01-introduction.html" in decision.retry_message
553
+
554
+
500555
 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
501556
     temp_dir: Path,
502557
 ) -> None: