Allow explicit operator resume after technical response repairs

This commit is contained in:
Nico committed 2026-10-20 21:20:46 +02:00
1 parent ecec96ce14
commit 167d8d5f5e
4 files changed
+85 -2

No files matched your search

+18
View File
@@ -55,3 +55,21 @@ Betroffen sind `test_model_loop_uses_project_tools_and_compact_previous_result`
(Varianten True, unknown, upgrade), `test_started_project_loop_keeps_legacy_prompt_protocol`
und `test_budgeted_evidence_and_portable_hybrid_resume`. Keine dieser bestehenden
Grenzen wurde für diese Änderung gelockert. `git diff --check` ohne Befund.
## Explizite Wiederaufnahme nach technischer Reparatur
`CheckpointStore.advance(..., manual_retry=True)` erlaubt die vom Betreiber
bewusst angeforderte sofortige Wiederaufnahme eines mit `INVALID_LLM_RESPONSES`
blockierten Falls ausschließlich in dessen gespeicherten Fortsetzungszustand.
Ohne dieses ausdrückliche Argument gilt weiterhin `retry_not_before`.
Die manuelle Wiederaufnahme darf keine Artefakte oder zusätzlichen Dateien
ändern. Sie setzt weder Budget noch fachliche/technische Untersuchungsstände
zurück. `manual_retry: true` wird im neuen unveränderlichen Lifecycle-Checkpoint
protokolliert. Andere Blockierungsgründe, insbesondere `AWAITING_TEST_FEEDBACK`,
werden dadurch nicht entsperrt. Idempotenz und Generationserwartung bleiben aktiv.
65 lokale Tests für Checkpoints, Queue und asynchrone Tests bestanden. Bug17
wurde auf ausdrücklichen Nutzerwunsch von Generation 47 auf 48 fortgesetzt;
Artefakthashes und Budgeterhalt sind im separaten Wiederaufnahmebericht gesichert.
Keine kostenpflichtige LLM-Ausführung durch den Entwicklungsagenten.
@@ -0,0 +1,21 @@
{
"format": "viano-manual-test-resume",
"date": "2026-10-20",
"run": "bug17-bounded-full-20261020",
"from_generation": 47,
"to_generation": 48,
"state": "INVESTIGATING_HYPOTHESES",
"manual_retry": true,
"prior_retry_not_before": "2026-10-20T20:00:32.238127+00:00",
"reason": "Explicit user-requested test continuation after fixed technical tool failure",
"unchanged_artifact_sha256": {
"investigation_state": "598aeb46462122bd559cee1a7cb162e03db7f0414decb0a8c880d229f240478d",
"budget_ledger": "7e4519804334e16b775a17d89257883fb14c5a05e9d132ba8791156dc9a79c8a",
"case_interpretation": "0fd3c2a1efe0f229592b3038b66db26e704f2fb1a03b7321ceb76450e3f98cb4",
"hypotheses": "30494a06ce373f4c497b3964524cee21bdf5350eac515f7a0022619879d125c8"
},
"budgets_reset": false,
"invalid_responses_reset": false,
"paid_api_calls": 0,
"package_valid": true
}
@@ -283,7 +283,7 @@ class CheckpointStore:
reason: str | None = None, retry_not_before: str | None = None,
details: dict | None = None, additions: dict[str, bytes] | None = None,
artifacts: dict[str, str | None] | None = None,
expected_generation: int | None = None) -> Path:
expected_generation: int | None = None, manual_retry: bool = False) -> Path:
try:
if not isinstance(step_id, str) or not step_id.strip():
raise LifecycleError("INVALID_STEP", "A nonblank stable step_id is required")
@@ -293,6 +293,10 @@ class CheckpointStore:
raise LifecycleError("RESERVED_ARTIFACT", "Lifecycle checkpoint pointer is service-managed")
if expected_generation is not None and (isinstance(expected_generation, bool) or not isinstance(expected_generation, int) or expected_generation < 0):
raise LifecycleError("INVALID_GENERATION", "expected_generation must be a nonnegative integer")
if type(manual_retry) is not bool:
raise LifecycleError("INVALID_RETRY", "manual_retry must be boolean")
if manual_retry:
details = {**(details or {}), "manual_retry": True}
payload = {"target": target, "reason": reason, "retry_not_before": retry_not_before,
"details": details or {}, "artifacts": artifacts or {},
"additions": {name: hashlib.sha256(data).hexdigest() for name, data in (additions or {}).items()}}
@@ -335,7 +339,11 @@ class CheckpointStore:
and review.get('status')=='declined' and review.get('context_ref')==manifest['artifacts'].get('diagnosis_context')):
resume=State.INVESTIGATING_HYPOTHESES
check_transition(manifest["state"], target, resume)
if manifest["state"] == State.BLOCKED and not self.retry_due(manifest):
if manual_retry and (manifest["state"] != State.BLOCKED
or manifest["routing"]["reason"] != "INVALID_LLM_RESPONSES"
or target != resume or additions or artifacts):
raise LifecycleError("INVALID_RETRY", "Manual repair retry requires an unchanged technical-response block")
if manifest["state"] == State.BLOCKED and not manual_retry and not self.retry_due(manifest):
raise LifecycleError("RETRY_NOT_DUE", "Retry timestamp has not been reached")
routing = {"destination": "cases", "reason": None, "retry_not_before": None}
if target == State.BLOCKED:
+36
View File
@@ -270,3 +270,39 @@ def test_empty_array_details_rejected(admitted):
with pytest.raises(LifecycleError):
store.advance("case-A", "stable-package", State.BUILDING_CASE, step_id="bad", details=[])
assert store.latest("case-A", "stable-package") == original
@pytest.mark.parametrize("reason", ["INVALID_LLM_RESPONSES", "RESEARCH_UNAVAILABLE", "AWAITING_TEST_FEEDBACK"])
def test_manual_repair_retry_is_explicit_scoped_and_preserves_content(admitted,reason):
_, store, _ = admitted
blocked = store.advance("case-A", "stable-package", State.BLOCKED, step_id="repair-block",
reason=reason, retry_not_before="2099-01-01T00:00:00Z")
with pytest.raises(LifecycleError):
store.advance("case-A", "stable-package", State.VALIDATED, step_id="automatic-too-early")
if reason != "INVALID_LLM_RESPONSES":
with pytest.raises(LifecycleError):
store.advance("case-A", "stable-package", State.VALIDATED, step_id="not-repairable",manual_retry=True)
assert store.latest("case-A", "stable-package") == blocked
return
with PackageReader(blocked) as reader:
before = reader.manifest
resumed = store.advance("case-A", "stable-package", State.VALIDATED, step_id="operator-repaired",manual_retry=True)
assert store.advance("case-A", "stable-package", State.VALIDATED, step_id="operator-repaired",manual_retry=True) == resumed
with PackageReader(resumed) as reader:
assert reader.read_bytes("unknown") == b"keep"
assert reader.manifest["analysis_revision"] == before["analysis_revision"]
assert reader.manifest["artifacts"].keys() == before["artifacts"].keys()
assert reader.manifest["routing"]["retry_not_before"] is None
assert checkpoint_chain(reader)[0][1]["details"]["manual_retry"] is True
assert PackageValidator().validate(resumed).valid
@pytest.mark.parametrize("changes", [{"additions":{"other":b"no"}}, {"artifacts":{"case":"other"}}, {"manual_retry":1}])
def test_manual_repair_retry_cannot_mutate_artifacts(admitted,changes):
_, store, _ = admitted
blocked = store.advance("case-A", "stable-package", State.BLOCKED, step_id="block-repair",
reason="INVALID_LLM_RESPONSES",retry_not_before="2099-01-01T00:00:00Z")
with pytest.raises(LifecycleError):
store.advance("case-A", "stable-package", State.VALIDATED, step_id="bad-repair",
**{**{"manual_retry":True},**changes})
assert store.latest("case-A", "stable-package") == blocked