Anchor stale HTML repairs
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
5e2425f67fcc34e23aeaac86454abb76de2af51a- Parents
-
6236b4d - Tree
2b388e9
5e2425f
5e2425f67fcc34e23aeaac86454abb76de2af51a6236b4d
2b388e9| Status | File | + | - |
|---|---|---|---|
| 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 | ||
| 23 | 23 | from .repair_focus import ( |
| 24 | 24 | ActiveRepairContext, |
| 25 | 25 | extract_active_repair_context, |
| 26 | + html_quality_repair_insertion_anchor, | |
| 26 | 27 | html_repair_issue_is_structural, |
| 27 | 28 | recent_repair_mutation_context_failed, |
| 28 | 29 | repair_line_is_html_quality, |
@@ -877,11 +878,32 @@ class ResponseRepairer: | ||
| 877 | 878 | if issue_line: |
| 878 | 879 | lines.append(f"- Current verifier issue: {issue_line[2:] if issue_line.startswith('- ') else issue_line}") |
| 879 | 880 | 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( | |
| 881 | 882 | self.context.session.messages, |
| 882 | 883 | target, |
| 883 | 884 | ) |
| 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: | |
| 885 | 907 | structural_suffix = ( |
| 886 | 908 | " Ensure the replacement has exactly one closing `</body>` tag, " |
| 887 | 909 | "exactly one closing `</html>` tag, and no content after `</html>`." |
src/loader/runtime/repair_focus.pymodified@@ -41,6 +41,8 @@ _HTML_STRUCTURAL_REPAIR_MARKERS = ( | ||
| 41 | 41 | "content appears after closing </html>", |
| 42 | 42 | "closing </body> appears after closing </html>", |
| 43 | 43 | ) |
| 44 | +_HTML_CLOSE_RE = re.compile(r"</html\s*>", re.IGNORECASE) | |
| 45 | +_BODY_CLOSE_RE = re.compile(r"</body\s*>", re.IGNORECASE) | |
| 44 | 46 | |
| 45 | 47 | |
| 46 | 48 | @dataclass(frozen=True) |
@@ -193,6 +195,36 @@ def html_repair_issue_is_structural(line: str) -> bool: | ||
| 193 | 195 | return any(marker in lowered for marker in _HTML_STRUCTURAL_REPAIR_MARKERS) |
| 194 | 196 | |
| 195 | 197 | |
| 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 | + | |
| 196 | 228 | def normalize_repair_path(raw_path: str) -> str: |
| 197 | 229 | text = str(raw_path or "").strip() |
| 198 | 230 | if not text: |
src/loader/runtime/tool_batches.pymodified@@ -36,6 +36,7 @@ from .policy_timeline import append_verification_timeline_entry | ||
| 36 | 36 | from .recovery import RecoveryContext, detect_missing_mutation_payload |
| 37 | 37 | from .repair_focus import ( |
| 38 | 38 | extract_active_repair_context, |
| 39 | + html_quality_repair_insertion_anchor, | |
| 39 | 40 | html_repair_issue_is_structural, |
| 40 | 41 | path_within_allowed_roots, |
| 41 | 42 | recent_repair_mutation_context_failed, |
@@ -606,6 +607,19 @@ class ToolBatchRunner: | ||
| 606 | 607 | self.context.recovery_context, |
| 607 | 608 | ) |
| 608 | 609 | 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 | |
| 609 | 623 | self.context.queue_steering_message( |
| 610 | 624 | "Reuse the earlier observation instead of repeating it. " |
| 611 | 625 | f"The last edit on `{edit_mismatch_target}` failed because `old_string` " |
@@ -2045,11 +2059,27 @@ class ToolBatchRunner: | ||
| 2045 | 2059 | else f"- Improve `{target}` until it satisfies the active content-quality verifier.\n" |
| 2046 | 2060 | ) |
| 2047 | 2061 | 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( | |
| 2049 | 2063 | self.context.session.messages, |
| 2050 | 2064 | target, |
| 2051 | 2065 | ) |
| 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: | |
| 2053 | 2083 | structural_suffix = ( |
| 2054 | 2084 | " Ensure the replacement has exactly one closing `</body>` tag, " |
| 2055 | 2085 | "exactly one closing `</html>` tag, and no content after `</html>`." |
src/loader/runtime/turn_completion.pymodified@@ -29,6 +29,7 @@ from .policy_timeline import ( | ||
| 29 | 29 | from .repair import ResponseRepairer |
| 30 | 30 | from .repair_focus import ( |
| 31 | 31 | extract_active_repair_context, |
| 32 | + html_quality_repair_insertion_anchor, | |
| 32 | 33 | html_repair_issue_is_structural, |
| 33 | 34 | recent_repair_mutation_context_failed, |
| 34 | 35 | repair_line_is_html_quality, |
@@ -549,6 +550,28 @@ def _build_html_quality_repair_continuation( | ||
| 549 | 550 | ) |
| 550 | 551 | issue_sentence = f" Current verifier issue: {issue_line}" if issue_line else "" |
| 551 | 552 | 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 | + | |
| 552 | 575 | force_write = stale_context or structural_issue |
| 553 | 576 | if force_write: |
| 554 | 577 | structural_sentence = ( |
tests/test_repair.pymodified@@ -321,7 +321,7 @@ def test_empty_response_retry_during_html_quality_repair_shrinks_mutation( | ||
| 321 | 321 | assert f"`{second_chapter.resolve(strict=False)}`" in decision.retry_message |
| 322 | 322 | |
| 323 | 323 | |
| 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( | |
| 325 | 325 | temp_dir: Path, |
| 326 | 326 | ) -> None: |
| 327 | 327 | context = build_context( |
@@ -367,8 +367,13 @@ def test_empty_response_retry_forces_write_after_stale_quality_repair_context( | ||
| 367 | 367 | |
| 368 | 368 | assert decision.should_continue is True |
| 369 | 369 | 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 | |
| 372 | 377 | |
| 373 | 378 | |
| 374 | 379 | 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 | ||
| 4425 | 4425 | assert dod.completed_items == completed_before_todowrite |
| 4426 | 4426 | |
| 4427 | 4427 | |
| 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( | |
| 4429 | 4429 | temp_dir: Path, |
| 4430 | 4430 | ) -> None: |
| 4431 | 4431 | async def assess_confidence( |
@@ -4482,9 +4482,11 @@ def test_todowrite_quality_repair_nudge_forces_write_after_stale_context( | ||
| 4482 | 4482 | |
| 4483 | 4483 | assert queued_messages |
| 4484 | 4484 | 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 | |
| 4488 | 4490 | |
| 4489 | 4491 | |
| 4490 | 4492 | @pytest.mark.asyncio |
tests/test_turn_completion.pymodified@@ -456,7 +456,7 @@ async def test_turn_completion_uses_quality_repair_prompt_for_rewrite_narration( | ||
| 456 | 456 | |
| 457 | 457 | |
| 458 | 458 | @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( | |
| 460 | 460 | temp_dir: Path, |
| 461 | 461 | ) -> None: |
| 462 | 462 | backend = ScriptedBackend() |
@@ -533,8 +533,10 @@ async def test_turn_completion_forces_write_after_stale_quality_repair_context( | ||
| 533 | 533 | assert decision.action == TurnCompletionAction.CONTINUE |
| 534 | 534 | message = agent.session.messages[-1].content |
| 535 | 535 | 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 | |
| 538 | 540 | |
| 539 | 541 | |
| 540 | 542 | @pytest.mark.asyncio |