Allow explicit operator resume after technical response repairs
This commit is contained in:
1 parent
ecec96ce14
commit
167d8d5f5e
4 files changed
+85
-2
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user