tenseleyflow/loader / 5e2425f

Browse files

Anchor stale HTML repairs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5e2425f67fcc34e23aeaac86454abb76de2af51a
Parents
6236b4d
Tree
2b388e9

7 changed files

StatusFile+-
M src/loader/runtime/repair.py 24 2
M src/loader/runtime/repair_focus.py 32 0
M src/loader/runtime/tool_batches.py 32 2
M src/loader/runtime/turn_completion.py 23 0
M tests/test_repair.py 8 3
M tests/test_tool_batches.py 6 4
M tests/test_turn_completion.py 5 3
src/loader/runtime/repair.pymodified
@@ -23,6 +23,7 @@ from .recovery import detect_missing_mutation_payload
2323
 from .repair_focus import (
2424
     ActiveRepairContext,
2525
     extract_active_repair_context,
26
+    html_quality_repair_insertion_anchor,
2627
     html_repair_issue_is_structural,
2728
     recent_repair_mutation_context_failed,
2829
     repair_line_is_html_quality,
@@ -877,11 +878,32 @@ class ResponseRepairer:
877878
         if issue_line:
878879
             lines.append(f"- Current verifier issue: {issue_line[2:] if issue_line.startswith('- ') else issue_line}")
879880
         structural_issue = html_repair_issue_is_structural(issue_line)
880
-        force_write = structural_issue or recent_repair_mutation_context_failed(
881
+        stale_context = recent_repair_mutation_context_failed(
881882
             self.context.session.messages,
882883
             target,
883884
         )
884
-        if force_write:
885
+        insertion_anchor = (
886
+            html_quality_repair_insertion_anchor(target)
887
+            if stale_context and not structural_issue
888
+            else None
889
+        )
890
+        if insertion_anchor:
891
+            lines.extend(
892
+                [
893
+                    "- Recent `edit`/`patch` attempts for this same target failed "
894
+                    "against stale or malformed context, but the current file has a "
895
+                    "safe closing-tail insertion anchor. Use exactly one "
896
+                    "`edit(file_path=..., old_string=..., new_string=...)` call now.",
897
+                    "- Use this exact `old_string` value from the current file:\n"
898
+                    f"```html\n{insertion_anchor}\n```",
899
+                    "- Set `new_string` to the substantive new body sections followed "
900
+                    "by that exact `old_string`, so the added content lands before the "
901
+                    "closing body/html tags.",
902
+                    "- Do not call `read`, `patch`, `write`, TodoWrite, or a final "
903
+                    "summary on this retry; emit the `edit` mutation tool call now.",
904
+                ]
905
+            )
906
+        elif structural_issue or stale_context:
885907
             structural_suffix = (
886908
                 " Ensure the replacement has exactly one closing `</body>` tag, "
887909
                 "exactly one closing `</html>` tag, and no content after `</html>`."
src/loader/runtime/repair_focus.pymodified
@@ -41,6 +41,8 @@ _HTML_STRUCTURAL_REPAIR_MARKERS = (
4141
     "content appears after closing </html>",
4242
     "closing </body> appears after closing </html>",
4343
 )
44
+_HTML_CLOSE_RE = re.compile(r"</html\s*>", re.IGNORECASE)
45
+_BODY_CLOSE_RE = re.compile(r"</body\s*>", re.IGNORECASE)
4446
 
4547
 
4648
 @dataclass(frozen=True)
@@ -193,6 +195,36 @@ def html_repair_issue_is_structural(line: str) -> bool:
193195
     return any(marker in lowered for marker in _HTML_STRUCTURAL_REPAIR_MARKERS)
194196
 
195197
 
198
+def html_quality_repair_insertion_anchor(raw_path: str) -> str | None:
199
+    """Return an exact on-disk closing-tail anchor for bounded HTML expansion."""
200
+
201
+    normalized = normalize_repair_path(raw_path)
202
+    if not normalized:
203
+        return None
204
+    path = Path(normalized)
205
+    if not path.is_file():
206
+        return None
207
+    try:
208
+        text = path.read_text()
209
+    except (OSError, UnicodeDecodeError):
210
+        return None
211
+
212
+    body_matches = list(_BODY_CLOSE_RE.finditer(text))
213
+    html_matches = list(_HTML_CLOSE_RE.finditer(text))
214
+    if len(body_matches) != 1 or len(html_matches) != 1:
215
+        return None
216
+    body_match = body_matches[0]
217
+    html_match = html_matches[0]
218
+    if body_match.start() > html_match.start():
219
+        return None
220
+    if text[html_match.end() :].strip():
221
+        return None
222
+    anchor = text[body_match.start() :].rstrip()
223
+    if not anchor.strip():
224
+        return None
225
+    return anchor
226
+
227
+
196228
 def normalize_repair_path(raw_path: str) -> str:
197229
     text = str(raw_path or "").strip()
198230
     if not text:
src/loader/runtime/tool_batches.pymodified
@@ -36,6 +36,7 @@ from .policy_timeline import append_verification_timeline_entry
3636
 from .recovery import RecoveryContext, detect_missing_mutation_payload
3737
 from .repair_focus import (
3838
     extract_active_repair_context,
39
+    html_quality_repair_insertion_anchor,
3940
     html_repair_issue_is_structural,
4041
     path_within_allowed_roots,
4142
     recent_repair_mutation_context_failed,
@@ -606,6 +607,19 @@ class ToolBatchRunner:
606607
             self.context.recovery_context,
607608
         )
608609
         if edit_mismatch_target and _tool_call_targets_path(tool_call, edit_mismatch_target):
610
+            insertion_anchor = html_quality_repair_insertion_anchor(edit_mismatch_target)
611
+            if insertion_anchor:
612
+                self.context.queue_steering_message(
613
+                    "Reuse the earlier observation instead of repeating it. "
614
+                    f"The last edit on `{edit_mismatch_target}` failed because `old_string` "
615
+                    "did not exactly match the current file. Use this exact current "
616
+                    "closing-tail anchor as `old_string` in one concrete `edit` call:\n"
617
+                    f"```html\n{insertion_anchor}\n```\n"
618
+                    "Set `new_string` to the substantive added body sections followed "
619
+                    "by that exact `old_string`, so the new content lands before the "
620
+                    "closing body/html tags. Do not read the same file again first."
621
+                )
622
+                return
609623
             self.context.queue_steering_message(
610624
                 "Reuse the earlier observation instead of repeating it. "
611625
                 f"The last edit on `{edit_mismatch_target}` failed because `old_string` "
@@ -2045,11 +2059,27 @@ class ToolBatchRunner:
20452059
                 else f"- Improve `{target}` until it satisfies the active content-quality verifier.\n"
20462060
             )
20472061
             structural_repair = html_repair_issue_is_structural(repair_issue)
2048
-            force_write = recent_repair_mutation_context_failed(
2062
+            stale_context = recent_repair_mutation_context_failed(
20492063
                 self.context.session.messages,
20502064
                 target,
20512065
             )
2052
-            if force_write or structural_repair:
2066
+            insertion_anchor = (
2067
+                html_quality_repair_insertion_anchor(target)
2068
+                if stale_context and not structural_repair
2069
+                else None
2070
+            )
2071
+            if insertion_anchor:
2072
+                immediate_step = (
2073
+                    f"- Immediate next step: edit `{target}`.\n"
2074
+                    "- Use one `edit(file_path=..., old_string=..., new_string=...)` "
2075
+                    "call anchored on the current closing document tail.\n"
2076
+                    "- Use this exact current closing-tail anchor as `old_string`:\n"
2077
+                    f"```html\n{insertion_anchor}\n```\n"
2078
+                    "- Set `new_string` to the substantive added body sections followed "
2079
+                    "by that exact `old_string`; do not call `read`, `patch`, `write`, "
2080
+                    "or TodoWrite again first."
2081
+                )
2082
+            elif stale_context or structural_repair:
20532083
                 structural_suffix = (
20542084
                     " Ensure the replacement has exactly one closing `</body>` tag, "
20552085
                     "exactly one closing `</html>` tag, and no content after `</html>`."
src/loader/runtime/turn_completion.pymodified
@@ -29,6 +29,7 @@ from .policy_timeline import (
2929
 from .repair import ResponseRepairer
3030
 from .repair_focus import (
3131
     extract_active_repair_context,
32
+    html_quality_repair_insertion_anchor,
3233
     html_repair_issue_is_structural,
3334
     recent_repair_mutation_context_failed,
3435
     repair_line_is_html_quality,
@@ -549,6 +550,28 @@ def _build_html_quality_repair_continuation(
549550
     )
550551
     issue_sentence = f" Current verifier issue: {issue_line}" if issue_line else ""
551552
     structural_issue = html_repair_issue_is_structural(issue_line)
553
+    insertion_anchor = (
554
+        html_quality_repair_insertion_anchor(target_text)
555
+        if stale_context and not structural_issue
556
+        else None
557
+    )
558
+    if insertion_anchor:
559
+        prompt = (
560
+            "[CONTINUE QUALITY REPAIR]\n"
561
+            "You just described a content-quality repair, but did not execute it. "
562
+            "Recent `patch`/`edit` attempts for this same file failed because their "
563
+            "remembered context was stale or malformed, so anchor the mutation on the "
564
+            "current closing document tail. "
565
+            f"Emit exactly one `edit(file_path=..., old_string=..., new_string=...)` tool call for `{target_text}` now."
566
+            f"{issue_sentence} "
567
+            "Use this exact `old_string` value from the current file:\n"
568
+            f"```html\n{insertion_anchor}\n```\n"
569
+            "Set `new_string` to the substantive new body sections followed by that exact "
570
+            "`old_string`, so content is inserted before the closing body/html tags. "
571
+            "Do not call `read`, `patch`, `write`, `TodoWrite`, or summarize."
572
+        )
573
+        return InProgressContinuation(prompt=prompt, target=None)
574
+
552575
     force_write = stale_context or structural_issue
553576
     if force_write:
554577
         structural_sentence = (
tests/test_repair.pymodified
@@ -321,7 +321,7 @@ def test_empty_response_retry_during_html_quality_repair_shrinks_mutation(
321321
     assert f"`{second_chapter.resolve(strict=False)}`" in decision.retry_message
322322
 
323323
 
324
-def test_empty_response_retry_forces_write_after_stale_quality_repair_context(
324
+def test_empty_response_retry_uses_exact_anchor_after_stale_quality_repair_context(
325325
     temp_dir: Path,
326326
 ) -> None:
327327
     context = build_context(
@@ -367,8 +367,13 @@ def test_empty_response_retry_forces_write_after_stale_quality_repair_context(
367367
 
368368
     assert decision.should_continue is True
369369
     assert decision.retry_message is not None
370
-    assert "Use exactly one `write(file_path=..., content=...)`" in decision.retry_message
371
-    assert "Do not call `read`, `edit`, `patch`, TodoWrite" in decision.retry_message
370
+    assert (
371
+        "Use exactly one `edit(file_path=..., old_string=..., new_string=...)`"
372
+        in decision.retry_message
373
+    )
374
+    assert "Use this exact `old_string` value from the current file" in decision.retry_message
375
+    assert "```html\n</body></html>\n```" in decision.retry_message
376
+    assert "Do not call `read`, `patch`, `write`, TodoWrite" in decision.retry_message
372377
 
373378
 
374379
 def test_empty_response_retry_forces_write_for_structural_html_repair(
tests/test_tool_batches.pymodified
@@ -4425,7 +4425,7 @@ async def test_tool_batch_runner_todowrite_during_quality_repair_requires_mutati
44254425
     assert dod.completed_items == completed_before_todowrite
44264426
 
44274427
 
4428
-def test_todowrite_quality_repair_nudge_forces_write_after_stale_context(
4428
+def test_todowrite_quality_repair_nudge_uses_exact_anchor_after_stale_context(
44294429
     temp_dir: Path,
44304430
 ) -> None:
44314431
     async def assess_confidence(
@@ -4482,9 +4482,11 @@ def test_todowrite_quality_repair_nudge_forces_write_after_stale_context(
44824482
 
44834483
     assert queued_messages
44844484
     message = queued_messages[-1]
4485
-    assert f"Immediate next step: rewrite `{chapter_one.resolve(strict=False)}`" in message
4486
-    assert "`write(file_path=..., content=...)`" in message
4487
-    assert "do not call `read`, `edit`, `patch`, or TodoWrite again first" in message
4485
+    assert f"Immediate next step: edit `{chapter_one.resolve(strict=False)}`" in message
4486
+    assert "`edit(file_path=..., old_string=..., new_string=...)`" in message
4487
+    assert "Use this exact current closing-tail anchor as `old_string`" in message
4488
+    assert "```html\n</body></html>\n```" in message
4489
+    assert "do not call `read`, `patch`, `write`, or TodoWrite again first" in message
44884490
 
44894491
 
44904492
 @pytest.mark.asyncio
tests/test_turn_completion.pymodified
@@ -456,7 +456,7 @@ async def test_turn_completion_uses_quality_repair_prompt_for_rewrite_narration(
456456
 
457457
 
458458
 @pytest.mark.asyncio
459
-async def test_turn_completion_forces_write_after_stale_quality_repair_context(
459
+async def test_turn_completion_uses_exact_anchor_after_stale_quality_repair_context(
460460
     temp_dir: Path,
461461
 ) -> None:
462462
     backend = ScriptedBackend()
@@ -533,8 +533,10 @@ async def test_turn_completion_forces_write_after_stale_quality_repair_context(
533533
     assert decision.action == TurnCompletionAction.CONTINUE
534534
     message = agent.session.messages[-1].content
535535
     assert message.startswith("[CONTINUE QUALITY REPAIR]")
536
-    assert "exactly one `write(file_path=..., content=...)`" in message
537
-    assert "Do not call `read`, `edit`, `patch`, `TodoWrite`, or summarize." in message
536
+    assert "exactly one `edit(file_path=..., old_string=..., new_string=...)`" in message
537
+    assert "Use this exact `old_string` value from the current file" in message
538
+    assert "```html\n</body></html>\n```" in message
539
+    assert "Do not call `read`, `patch`, `write`, `TodoWrite`, or summarize." in message
538540
 
539541
 
540542
 @pytest.mark.asyncio