From 3231e1af07a8a4e34d9ea2e52a7f8b938a2e6e17 Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 13 Sep 2026 18:09:47 -0500 Subject: [PATCH] hermes: recover structured provider completion records --- .../hermes/scripts/scm_resume_bootstrap.py | 29 +++++++++++++++---- .../test_hermes_execution_pool_submission.py | 15 ++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/services/hermes/scripts/scm_resume_bootstrap.py b/services/hermes/scripts/scm_resume_bootstrap.py index 2f362266..571a8c79 100755 --- a/services/hermes/scripts/scm_resume_bootstrap.py +++ b/services/hermes/scripts/scm_resume_bootstrap.py @@ -22,6 +22,26 @@ PART = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z") RESULT_FIELDS = frozenset({"status", "summary", "changed_files", "tests_run", "artifacts", "findings", "blockers"}) +def _result_candidate(value: Any) -> dict[str, Any] | None: + """Accept a direct result or the one canonical Claude session result field.""" + if not isinstance(value, dict): + return None + candidate = value.get("structured_output") if "structured_output" in value else value + if not isinstance(candidate, dict) or set(candidate) != RESULT_FIELDS: + return None + if candidate.get("status") != "completed": + return None + if not isinstance(candidate.get("summary"), str) or not candidate["summary"].strip(): + return None + if any( + not isinstance(candidate.get(name), list) + or any(not isinstance(item, str) for item in candidate[name]) + for name in RESULT_FIELDS - {"status", "summary"} + ): + return None + return candidate + + def _completed_result(assignment: dict[str, Any]) -> dict[str, Any]: """Return exactly one completed result JSON object from an ordinal's safe log.""" parts = tuple(str(assignment[name]) for name in ("board", "task_id", "run_id")) @@ -54,13 +74,10 @@ def _completed_result(assignment: dict[str, Any]) -> dict[str, Any]: value = json.loads(line) except (UnicodeDecodeError, json.JSONDecodeError): continue - if not isinstance(value, dict) or set(value) != RESULT_FIELDS or value.get("status") != "completed": + candidate = _result_candidate(value) + if candidate is None: continue - if not isinstance(value.get("summary"), str) or not value["summary"].strip(): - continue - if any(not isinstance(value.get(name), list) or any(not isinstance(item, str) for item in value[name]) for name in RESULT_FIELDS - {"status", "summary"}): - continue - candidates[canonical_json(value)] = value + candidates[canonical_json(candidate)] = candidate if len(candidates) != 1: raise ProtocolError("bootstrap result evidence is absent or ambiguous") return next(iter(candidates.values())) diff --git a/testing/tests/test_hermes_execution_pool_submission.py b/testing/tests/test_hermes_execution_pool_submission.py index 8a1af466..bea48efe 100644 --- a/testing/tests/test_hermes_execution_pool_submission.py +++ b/testing/tests/test_hermes_execution_pool_submission.py @@ -444,6 +444,21 @@ def test_mediator_bootstrap_recovers_one_unique_completed_log_result(tmp_path, m assert seen["structured"] == completed and seen["request"]["title"] == completed["summary"] +def test_mediator_bootstrap_recovers_one_unique_nested_session_result(tmp_path, monkeypatch): + """A Claude stream carries the completed result under structured_output.""" + exact = protocol.sign_envelope( + KEY, "assignment", binding(), payload(root_task_id="t_deadbeef", continuation_kind="repair") + ) + log = tmp_path / "session-state" / "metis" / "t_deadbeef" / "42.log" + log.parent.mkdir(parents=True) + completed = dict(RESULT) + event = {"event": "result", "structured_output": completed} + log.write_bytes(protocol.canonical_json(event) + b"\n" + protocol.canonical_json(event) + b"\n") + monkeypatch.setattr(resume_bootstrap, "ROOT", tmp_path) + + assert resume_bootstrap._completed_result(exact) == completed + + def test_mediator_bootstrap_rejects_a_symlinked_log_ancestor(tmp_path, monkeypatch): exact = protocol.sign_envelope(KEY, "assignment", binding(), payload()) outside = tmp_path / "outside"