This is an automated email from the ASF dual-hosted git repository.

shuke987 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 612896bff0d [improvement](ci) Verify Litefuse subagent trace 
completeness (#67567)
612896bff0d is described below

commit 612896bff0d1f65d33b5e216157f8d8e7082948f
Author: shuke <[email protected]>
AuthorDate: Mon Sep 7 21:05:38 2026 +0800

    [improvement](ci) Verify Litefuse subagent trace completeness (#67567)
    
    The Litefuse exporter currently posts both the main review trace and
    selected subagent session traces, but `--verify` only reads back the
    main trace. An OTLP HTTP success does not prove that all subagent
    observations have become queryable, so a complete main trace can hide
    partially ingested or missing subagent traces.
---
 .github/scripts/emit_litefuse_otel_io.py      | 454 ++++++++++++++++----------
 .github/scripts/test_emit_litefuse_otel_io.py | 364 ++++++++++++++++++++-
 .github/workflows/code-review-runner.yml      |   1 +
 3 files changed, 635 insertions(+), 184 deletions(-)

diff --git a/.github/scripts/emit_litefuse_otel_io.py 
b/.github/scripts/emit_litefuse_otel_io.py
index e3a33f377b3..53ef46949af 100644
--- a/.github/scripts/emit_litefuse_otel_io.py
+++ b/.github/scripts/emit_litefuse_otel_io.py
@@ -1592,26 +1592,42 @@ def post_payload(
     }
 
 
-def fetch_trace(base_url, public_key, secret_key, trace_id):
+def verification_request_timeout(deadline):
+    if deadline is None:
+        return 30
+    remaining = deadline - time.monotonic()
+    if remaining <= 0:
+        raise TimeoutError("Litefuse verification deadline exhausted")
+    return min(30, remaining)
+
+
+def fetch_trace(base_url, public_key, secret_key, trace_id, *, deadline=None):
     auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
     request = urllib.request.Request(
         f"{base_url.rstrip('/')}/api/public/traces/{trace_id}",
         headers={"Authorization": f"Basic {auth}"},
         method="GET",
     )
-    with urllib.request.urlopen(request, timeout=30) as response:
+    with urllib.request.urlopen(
+        request, timeout=verification_request_timeout(deadline)
+    ) as response:
         return json.loads(response.read().decode())
 
 
 def fetch_observations_v2(
-    base_url, public_key, secret_key, trace_id, max_pages=10
+    base_url, public_key, secret_key, trace_id, max_pages=10, *,
+    deadline=None, start_time=None, end_time=None,
 ):
     auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
     now = datetime.now(timezone.utc)
     query = {
         "traceId": trace_id,
-        "fromStartTime": (now - 
timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
-        "toStartTime": (now + 
timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
+        "fromStartTime": start_time or (
+            now - timedelta(hours=1)
+        ).isoformat().replace("+00:00", "Z"),
+        "toStartTime": end_time or (
+            now + timedelta(minutes=5)
+        ).isoformat().replace("+00:00", "Z"),
         "fields": "core,basic,io,trace_context,model,usage",
         "limit": "1000",
     }
@@ -1626,7 +1642,9 @@ def fetch_observations_v2(
             headers={"Authorization": f"Basic {auth}"},
             method="GET",
         )
-        with urllib.request.urlopen(request, timeout=30) as response:
+        with urllib.request.urlopen(
+            request, timeout=verification_request_timeout(deadline)
+        ) as response:
             payload = json.loads(response.read().decode())
         rows.extend(observation_rows_from_v2(payload))
         meta = payload.get("meta") if isinstance(payload, dict) else {}
@@ -1640,7 +1658,7 @@ def fetch_observations_v2(
 
 
 def fetch_observations_legacy(
-    base_url, public_key, secret_key, trace_id, max_pages=10
+    base_url, public_key, secret_key, trace_id, max_pages=10, *, deadline=None
 ):
     auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
     limit = 100
@@ -1655,7 +1673,9 @@ def fetch_observations_legacy(
             headers={"Authorization": f"Basic {auth}"},
             method="GET",
         )
-        with urllib.request.urlopen(request, timeout=30) as response:
+        with urllib.request.urlopen(
+            request, timeout=verification_request_timeout(deadline)
+        ) as response:
             payload = json.loads(response.read().decode())
         last_payload = payload if isinstance(payload, dict) else {}
         page_rows = observation_rows_from_v2(last_payload)
@@ -1712,181 +1732,248 @@ def context_events_readback_ok(input_object):
     return event_count == 0 and events_value in (None, "", {})
 
 
-def verify_trace(
-    args, public_key, secret_key, trace_id, expected_observation_count
-):
-    last_diagnostic = {}
+def trace_verification_target(result, payload, *, subagent=False):
+    observations = [
+        event["body"]
+        for event in payload["batch"]
+        if event["type"] in ("span-create", "generation-create")
+    ]
+    start_times = [
+        datetime.fromisoformat(body["startTime"].replace("Z", "+00:00"))
+        for body in observations
+    ]
+    # Subagent sessions retain their original timestamps, including sessions
+    # older than the v2 reader's default one-hour window.
+    target = {
+        "result": result,
+        "subagent": subagent,
+        "start_time": (min(start_times) - timedelta(seconds=1)).isoformat(),
+        "end_time": (max(start_times) + timedelta(seconds=1)).isoformat(),
+    }
+    if subagent:
+        target["expected_io"] = {
+            otel_id(body["id"], 8): [
+                field for field in ("input", "output") if body.get(field) is 
not None
+            ]
+            for body in observations
+        }
+    return target
+
+
+def observation_has_expected_io(observation, target):
+    if not target["subagent"]:
+        return bool(observation.get("input") and observation.get("output"))
+    # Empty objects, lists and strings are legitimate session event payloads.
+    # Only require fields actually sent by the exporter (None is not exported).
+    fields = target["expected_io"].get(str(observation.get("id")), ())
+    return all(observation.get(field) is not None for field in fields)
+
+
+def inspect_trace(args, public_key, secret_key, target, deadline):
+    result = target["result"]
+    trace_id = result["trace_id"]
+    subagent = target["subagent"]
+    root_name = "codex.subagent.review" if subagent else "codex.review"
     required_observation_count = max(
-        args.min_observations, expected_observation_count
+        1 if subagent else args.min_observations, result["observation_count"]
     )
-    for _ in range(args.verify_attempts):
-        legacy_trace_error = ""
-        try:
-            legacy_detail = fetch_trace(args.base_url, public_key, secret_key, 
trace_id)
-        except Exception as exc:
-            legacy_detail = {}
-            legacy_trace_error = type(exc).__name__
+    legacy_trace_error = ""
+    try:
+        legacy_detail = fetch_trace(
+            args.base_url, public_key, secret_key, trace_id, deadline=deadline
+        )
+    except Exception as exc:
+        legacy_detail = {}
+        legacy_trace_error = type(exc).__name__
+    try:
+        observations_payload = fetch_observations_legacy(
+            args.base_url, public_key, secret_key, trace_id,
+            max_pages=max(10, required_observation_count // 100 + 1),
+            deadline=deadline,
+        )
+        observations = observation_rows_from_v2(observations_payload)
+        read_source = "legacy_observations"
+    except Exception as exc:
         try:
-            observations_payload = fetch_observations_legacy(
-                args.base_url, public_key, secret_key, trace_id
+            observations_payload = fetch_observations_v2(
+                args.base_url, public_key, secret_key, trace_id,
+                max_pages=max(10, required_observation_count // 1000 + 1),
+                deadline=deadline,
+                start_time=target.get("start_time"),
+                end_time=target.get("end_time"),
             )
             observations = observation_rows_from_v2(observations_payload)
-            read_source = "legacy_observations"
-        except Exception as exc:
-            try:
-                observations_payload = fetch_observations_v2(
-                    args.base_url, public_key, secret_key, trace_id
-                )
-                observations = observation_rows_from_v2(observations_payload)
-                read_source = "v2_observations"
-            except Exception:
-                observations = legacy_detail.get("observations") or []
-                read_source = f"legacy_trace_fallback:{type(exc).__name__}"
-        observations_missing_io = [
-            observation
-            for observation in observations
-            if not (observation.get("input") and observation.get("output"))
-        ]
-        observation_ids = [
-            str(observation.get("id"))
-            for observation in observations
-            if observation.get("id")
-        ]
-        unique_observation_count = len(set(observation_ids))
-        observations_missing_id_count = len(observations) - 
len(observation_ids)
-        duplicate_observation_count = len(observation_ids) - 
unique_observation_count
-        step_observations = [
-            observation
-            for observation in observations
-            if observation.get("name") not in ("codex.review", "codex.turn")
-        ]
-        agent_message_observations = [
-            observation
-            for observation in observations
-            if observation.get("name") == "codex.agent_message"
-        ]
-        agent_message_input_objects = [
-            observation_io_object(observation, "input")
-            for observation in agent_message_observations
-        ]
-        agent_message_input_keys = sorted(
-            {
-                key
-                for input_object in agent_message_input_objects
-                for key in input_object.keys()
-            }
-        )
-        agent_message_all_have_context_window = all(
-            bool(input_object.get("context_window"))
-            for input_object in agent_message_input_objects
-        )
-        agent_message_all_have_context_events = all(
-            context_events_readback_ok(input_object)
-            for input_object in agent_message_input_objects
-        )
-        agent_message_with_previous_count = sum(
-            1
-            for input_object in agent_message_input_objects
-            if bool(input_object.get("previous_agent_message"))
-        )
-        agent_message_with_turn_input_count = sum(
-            1
+            read_source = "v2_observations"
+        except Exception:
+            observations = legacy_detail.get("observations") or []
+            read_source = f"legacy_trace_fallback:{type(exc).__name__}"
+    observations_missing_io = [
+        observation
+        for observation in observations
+        if not observation_has_expected_io(observation, target)
+    ]
+    observation_ids = [
+        str(observation.get("id"))
+        for observation in observations
+        if observation.get("id")
+    ]
+    unique_observation_count = len(set(observation_ids))
+    observations_missing_id_count = len(observations) - len(observation_ids)
+    duplicate_observation_count = len(observation_ids) - 
unique_observation_count
+    step_observations = [
+        observation
+        for observation in observations
+        if observation.get("name") not in (root_name, "codex.turn")
+    ]
+    agent_message_observations = [
+        observation
+        for observation in observations
+        if observation.get("name") == "codex.agent_message"
+    ]
+    agent_message_input_objects = [
+        observation_io_object(observation, "input")
+        for observation in agent_message_observations
+    ]
+    agent_message_input_keys = sorted(
+        {
+            key
             for input_object in agent_message_input_objects
-            if bool(input_object.get("turn_input"))
+            for key in input_object.keys()
+        }
+    )
+    agent_message_all_have_context_window = all(
+        bool(input_object.get("context_window"))
+        for input_object in agent_message_input_objects
+    )
+    agent_message_all_have_context_events = all(
+        context_events_readback_ok(input_object)
+        for input_object in agent_message_input_objects
+    )
+    agent_message_with_previous_count = sum(
+        1
+        for input_object in agent_message_input_objects
+        if bool(input_object.get("previous_agent_message"))
+    )
+    agent_message_with_turn_input_count = sum(
+        1
+        for input_object in agent_message_input_objects
+        if bool(input_object.get("turn_input"))
+    )
+    agent_message_context_event_counts = [
+        (input_object.get("context_window") or {}).get("event_count", 0)
+        for input_object in agent_message_input_objects[:20]
+        if isinstance(input_object.get("context_window"), dict)
+    ]
+    agent_message_event_type_samples = [
+        context_event_types(
+            input_object.get("events_since_previous_agent_message")
+        )[:8]
+        for input_object in agent_message_input_objects[:5]
+    ]
+    agent_message_structure_ok = (
+        agent_message_all_have_context_window
+        and agent_message_all_have_context_events
+        and agent_message_with_turn_input_count == 1
+        and (
+            agent_message_with_previous_count
+            == max(len(agent_message_observations) - 1, 0)
         )
-        agent_message_context_event_counts = [
-            (input_object.get("context_window") or {}).get("event_count", 0)
-            for input_object in agent_message_input_objects[:20]
-            if isinstance(input_object.get("context_window"), dict)
-        ]
-        agent_message_event_type_samples = [
-            context_event_types(
-                input_object.get("events_since_previous_agent_message")
-            )[:8]
-            for input_object in agent_message_input_objects[:5]
+    )
+    root_observation = next(
+        (
+            observation for observation in observations
+            if observation.get("name") == root_name
+        ),
+        {},
+    )
+    trace_input = legacy_detail.get("input") or root_observation.get("input")
+    trace_output = legacy_detail.get("output") or 
root_observation.get("output")
+    expected_ids = set(target.get("expected_io", {}))
+    missing_expected_ids = sorted(expected_ids - set(observation_ids))
+    last_diagnostic = {
+        "read_source": read_source,
+        "root_observation": bool(root_observation),
+        "missing_expected_observation_count": len(missing_expected_ids),
+        "missing_expected_observation_ids": missing_expected_ids[:20],
+        "legacy_trace_input": bool(legacy_detail.get("input")),
+        "legacy_trace_output": bool(legacy_detail.get("output")),
+        "legacy_trace_error": legacy_trace_error,
+        "trace_input": bool(trace_input),
+        "trace_output": bool(trace_output),
+        "observation_count": len(observations),
+        "required_observation_count": required_observation_count,
+        "unique_observation_count": unique_observation_count,
+        "observations_missing_id_count": observations_missing_id_count,
+        "duplicate_observation_count": duplicate_observation_count,
+        "step_observation_count": len(step_observations),
+        "agent_message_count": len(agent_message_observations),
+        "agent_message_input_keys": agent_message_input_keys,
+        "agent_message_all_have_context_window": 
agent_message_all_have_context_window,
+        "agent_message_all_have_context_events": 
agent_message_all_have_context_events,
+        "agent_message_with_previous_count": agent_message_with_previous_count,
+        "agent_message_with_turn_input_count": 
agent_message_with_turn_input_count,
+        "agent_message_context_event_counts": 
agent_message_context_event_counts,
+        "agent_message_event_type_samples": agent_message_event_type_samples,
+        "observations_missing_io": [
+            observation.get("name") for observation in 
observations_missing_io[:20]
+        ],
+        "observation_names": [
+            observation.get("name") for observation in observations[:20]
+        ],
+    }
+    ok = all(
+        [
+            root_observation,
+            not missing_expected_ids,
+            trace_input,
+            trace_output,
+            unique_observation_count >= required_observation_count,
+            observations_missing_id_count == 0,
+            duplicate_observation_count == 0,
+            len(step_observations) >= (0 if subagent else 
args.min_step_observations),
+            not observations_missing_io,
+            not agent_message_observations or agent_message_structure_ok,
         ]
-        agent_message_structure_ok = (
-            agent_message_all_have_context_window
-            and agent_message_all_have_context_events
-            and agent_message_with_turn_input_count == 1
-            and (
-                agent_message_with_previous_count
-                == max(len(agent_message_observations) - 1, 0)
+    )
+    return ok, last_diagnostic
+
+
+def verify_traces(args, public_key, secret_key, targets):
+    # All exported traces share polling rounds and one elapsed-time budget.
+    # Never restart the wait budget for each subagent.
+    deadline = time.monotonic() + args.verify_timeout_seconds
+    pending = list(targets)
+    for target in pending:
+        target["result"]["verification_diagnostic"] = {"read_source": 
"not_polled"}
+    for attempt in range(args.verify_attempts):
+        for target in pending:
+            if time.monotonic() >= deadline:
+                break
+            ok, diagnostic = inspect_trace(
+                args, public_key, secret_key, target, deadline
             )
-        )
-        root_observation = next(
-            (observation for observation in observations if 
observation.get("name") == "codex.review"),
-            {},
-        )
-        trace_input = legacy_detail.get("input") or 
root_observation.get("input")
-        trace_output = legacy_detail.get("output") or 
root_observation.get("output")
-        last_diagnostic = {
-            "read_source": read_source,
-            "legacy_trace_input": bool(legacy_detail.get("input")),
-            "legacy_trace_output": bool(legacy_detail.get("output")),
-            "legacy_trace_error": legacy_trace_error,
-            "trace_input": bool(trace_input),
-            "trace_output": bool(trace_output),
-            "observation_count": len(observations),
-            "required_observation_count": required_observation_count,
-            "unique_observation_count": unique_observation_count,
-            "observations_missing_id_count": observations_missing_id_count,
-            "duplicate_observation_count": duplicate_observation_count,
-            "step_observation_count": len(step_observations),
-            "agent_message_count": len(agent_message_observations),
-            "agent_message_input_keys": agent_message_input_keys,
-            "agent_message_all_have_context_window": 
agent_message_all_have_context_window,
-            "agent_message_all_have_context_events": 
agent_message_all_have_context_events,
-            "agent_message_with_previous_count": 
agent_message_with_previous_count,
-            "agent_message_with_turn_input_count": 
agent_message_with_turn_input_count,
-            "agent_message_context_event_counts": 
agent_message_context_event_counts,
-            "agent_message_event_type_samples": 
agent_message_event_type_samples,
-            "observations_missing_io": [
-                observation.get("name") for observation in 
observations_missing_io[:20]
-            ],
-            "observation_names": [observation.get("name") for observation in 
observations[:20]],
-        }
-        ok = all(
-            [
-                trace_input,
-                trace_output,
-                unique_observation_count >= required_observation_count,
-                observations_missing_id_count == 0,
-                duplicate_observation_count == 0,
-                len(step_observations) >= args.min_step_observations,
-                not observations_missing_io,
-                not agent_message_observations or agent_message_structure_ok,
-            ]
-        )
-        if ok:
-            return {
-                "trace_input": True,
-                "trace_output": True,
-                "read_source": read_source,
-                "observation_count": len(observations),
-                "required_observation_count": required_observation_count,
-                "unique_observation_count": unique_observation_count,
-                "observations_missing_id_count": observations_missing_id_count,
-                "duplicate_observation_count": duplicate_observation_count,
-                "step_observation_count": len(step_observations),
-                "agent_message_count": len(agent_message_observations),
-                "agent_message_input_keys": agent_message_input_keys,
-                "agent_message_all_have_context_window": 
agent_message_all_have_context_window,
-                "agent_message_all_have_context_events": 
agent_message_all_have_context_events,
-                "agent_message_with_previous_count": 
agent_message_with_previous_count,
-                "agent_message_with_turn_input_count": 
agent_message_with_turn_input_count,
-                "agent_message_context_event_counts": 
agent_message_context_event_counts,
-                "agent_message_event_type_samples": 
agent_message_event_type_samples,
-                "observations_missing_io": [],
-                "observation_names": [
-                    observation.get("name") for observation in 
observations[:20]
-                ],
-            }
-        time.sleep(args.verify_sleep_seconds)
+            result = target["result"]
+            result["verification_diagnostic"] = diagnostic
+            if ok:
+                result["verified"] = diagnostic
+                del result["verification_diagnostic"]
+        pending = [
+            target for target in pending
+            if "verification_diagnostic" in target["result"]
+        ]
+        if not pending:
+            return
+        remaining = deadline - time.monotonic()
+        if remaining <= 0 or attempt + 1 == args.verify_attempts:
+            break
+        time.sleep(min(args.verify_sleep_seconds, remaining))
+    diagnostics = {
+        target["result"]["trace_id"]: 
target["result"]["verification_diagnostic"]
+        for target in pending
+    }
     raise RuntimeError(
-        "Litefuse trace "
-        f"{trace_id} did not expose multi-step I/O in time; "
-        f"last_diagnostic={json.dumps(last_diagnostic, sort_keys=True)}"
+        "Litefuse traces did not expose complete I/O in time; "
+        f"pending_traces={json.dumps(diagnostics, sort_keys=True)}"
     )
 
 
@@ -1923,6 +2010,10 @@ def parse_args():
     parser.add_argument("--dry-run", action="store_true")
     parser.add_argument("--verify-attempts", type=int, default=24)
     parser.add_argument("--verify-sleep-seconds", type=int, default=5)
+    parser.add_argument(
+        "--verify-timeout-seconds", type=int, default=120,
+        help="Shared elapsed-time budget for reading back all exported traces",
+    )
     parser.add_argument("--min-observations", type=int, default=3)
     parser.add_argument("--min-step-observations", type=int, default=1)
     return parser.parse_args()
@@ -2030,14 +2121,17 @@ def main():
         )
 
     if args.verify:
-        try:
-            result["verified"] = verify_trace(
-                args,
-                public_key,
-                secret_key,
-                trace_id,
-                observation_count,
+        targets = [trace_verification_target(result, payload)]
+        targets.extend(
+            trace_verification_target(
+                subagent_result, subagent_payload["payload"], subagent=True
+            )
+            for subagent_result, subagent_payload in zip(
+                result["subagent_traces"], subagent_payloads
             )
+        )
+        try:
+            verify_traces(args, public_key, secret_key, targets)
         except Exception as exc:
             result["verification_error"] = str(exc)
             print(json.dumps(result, sort_keys=True))
diff --git a/.github/scripts/test_emit_litefuse_otel_io.py 
b/.github/scripts/test_emit_litefuse_otel_io.py
index 3ccdb7b6d60..db2def32208 100644
--- a/.github/scripts/test_emit_litefuse_otel_io.py
+++ b/.github/scripts/test_emit_litefuse_otel_io.py
@@ -17,10 +17,12 @@
 # under the License.
 
 import importlib.util
+from contextlib import redirect_stdout
 import io
 import json
 from pathlib import Path
 from types import SimpleNamespace
+import sys
 import unittest
 from unittest import mock
 import urllib.error
@@ -74,6 +76,36 @@ class JsonResponse(FakeResponse):
 
 
 class LitefuseOtelExporterTest(unittest.TestCase):
+    def verify_single_trace(self, args, public_key, secret_key, trace_id, 
count):
+        result = {"trace_id": trace_id, "observation_count": count}
+        MODULE.verify_traces(
+            args, public_key, secret_key, [{"result": result, "subagent": 
False}]
+        )
+        return result["verified"]
+
+    def verification_args(self, **overrides):
+        return SimpleNamespace(**{
+            "base_url": "https://litefuse.example";,
+            "verify_attempts": 3,
+            "verify_sleep_seconds": 5,
+            "verify_timeout_seconds": 120,
+            "min_observations": 3,
+            "min_step_observations": 1,
+            **overrides,
+        })
+
+    def verification_fixture(self, *, subagent=False, count=3):
+        events = [self.span_event(f"{index:032x}") for index in range(1, count 
+ 1)]
+        events[0]["body"]["name"] = "codex.subagent.review" if subagent else 
"codex.review"
+        payload = {"batch": [{"type": "trace-create", "body": 
self.trace_body()}, *events]}
+        result = {"trace_id": "subagent" if subagent else "main", 
"observation_count": count}
+        target = MODULE.trace_verification_target(result, payload, 
subagent=subagent)
+        observations = [
+            {**event["body"], "id": MODULE.otel_id(event["body"]["id"], 8)}
+            for event in events
+        ]
+        return target, observations, payload
+
     def trace_body(self):
         return {
             "id": "1" * 32,
@@ -694,10 +726,331 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         self.assertEqual(status["request_count"], 1)
         self.assertEqual(status["success_count"], 1)
 
+    def 
test_verifies_pending_subagent_without_polling_complete_main_again(self):
+        main, main_rows, _ = self.verification_fixture()
+        child, child_rows, _ = self.verification_fixture(subagent=True)
+        replies = [main_rows, child_rows[:1], child_rows]
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy",
+            side_effect=[{"data": rows} for rows in replies],
+        ) as fetch, mock.patch.object(MODULE.time, "sleep") as sleep:
+            MODULE.verify_traces(self.verification_args(), "public", "secret", 
[main, child])
+
+        self.assertEqual([call.args[3] for call in fetch.call_args_list], 
["main", "subagent", "subagent"])
+        sleep.assert_called_once_with(5)
+        for target in (main, child):
+            
self.assertEqual(target["result"]["verified"]["observation_count"], 3)
+            self.assertNotIn("verification_diagnostic", target["result"])
+
+    def test_reports_all_missing_subagents_and_preserves_main_success(self):
+        main, main_rows, _ = self.verification_fixture()
+        children = [self.verification_fixture(subagent=True)[0] for _ in 
range(2)]
+        for index, child in enumerate(children):
+            child["result"]["trace_id"] = f"child-{index}"
+        replies = [main_rows, [], [], [], []]
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy",
+            side_effect=[{"data": rows} for rows in replies],
+        ), mock.patch.object(MODULE.time, "sleep") as sleep:
+            with self.assertRaisesRegex(RuntimeError, "child-0.*child-1"):
+                MODULE.verify_traces(
+                    self.verification_args(verify_attempts=2), "public", 
"secret",
+                    [main, *children],
+                )
+        self.assertIn("verified", main["result"])
+        for child in children:
+            self.assertNotIn("verified", child["result"])
+            self.assertEqual(
+                
child["result"]["verification_diagnostic"]["missing_expected_observation_count"],
 3
+            )
+        sleep.assert_called_once_with(5)
+
+    def 
test_subagent_profile_accepts_root_only_and_session_message_shapes(self):
+        for count in (1, 2):
+            with self.subTest(count=count):
+                target, rows, _ = self.verification_fixture(subagent=True, 
count=count)
+                if count == 2:
+                    rows[1]["name"] = "codex.subagent.message.assistant"
+                    rows[1]["input"] = {"role": "assistant"}
+                    rows[1]["output"] = {"text": "done"}
+                with mock.patch.object(MODULE, "fetch_trace", 
return_value={}), mock.patch.object(
+                    MODULE, "fetch_observations_legacy", return_value={"data": 
rows}
+                ):
+                    MODULE.verify_traces(self.verification_args(), "public", 
"secret", [target])
+                
self.assertEqual(target["result"]["verified"]["required_observation_count"], 
count)
+
+    def 
test_subagent_rejects_missing_root_id_or_io_even_when_count_is_sufficient(self):
+        for defect in ("root", "wrong_id", "duplicate", "empty_id", "input", 
"output"):
+            with self.subTest(defect=defect):
+                target, rows, _ = self.verification_fixture(subagent=True)
+                if defect == "root":
+                    rows[0]["name"] = "codex.command"
+                elif defect == "wrong_id":
+                    rows[1]["id"] = "unrelated-observation"
+                elif defect == "duplicate":
+                    rows[1]["id"] = rows[2]["id"]
+                elif defect == "empty_id":
+                    rows[1]["id"] = ""
+                else:
+                    rows[1][defect] = None
+                with mock.patch.object(
+                    MODULE, "fetch_trace", return_value={"input": "p", 
"output": "o"}
+                ), mock.patch.object(
+                    MODULE, "fetch_observations_legacy", return_value={"data": 
rows}
+                ):
+                    with self.assertRaisesRegex(RuntimeError, "subagent"):
+                        MODULE.verify_traces(
+                            self.verification_args(verify_attempts=1), 
"public", "secret", [target]
+                        )
+
+    def test_subagent_accepts_exported_empty_io_and_unexported_none(self):
+        for value in ({}, [], "", False, 0, None):
+            with self.subTest(value=value):
+                _, rows, payload = self.verification_fixture(subagent=True, 
count=2)
+                payload["batch"][-1]["body"]["output"] = value
+                result = {"trace_id": "subagent", "observation_count": 2}
+                target = MODULE.trace_verification_target(result, payload, 
subagent=True)
+                rows[1]["output"] = value
+                with mock.patch.object(MODULE, "fetch_trace", 
return_value={}), mock.patch.object(
+                    MODULE, "fetch_observations_legacy", return_value={"data": 
rows}
+                ):
+                    MODULE.verify_traces(self.verification_args(), "public", 
"secret", [target])
+                self.assertIn("verified", result)
+
+    def test_v2_fallback_uses_exported_session_time_window(self):
+        target, rows, payload = self.verification_fixture(subagent=True, 
count=2)
+        payload["batch"][-1]["body"]["startTime"] = "2026-09-01T02:00:00+00:00"
+        target = MODULE.trace_verification_target(target["result"], payload, 
subagent=True)
+        requests = []
+
+        def fake_urlopen(request, timeout):
+            requests.append(request)
+            if "/v2/observations?" not in request.full_url:
+                raise urllib.error.URLError("legacy API unavailable")
+            return JsonResponse({"data": rows, "meta": {}})
+
+        with mock.patch.object(MODULE.urllib.request, "urlopen", 
side_effect=fake_urlopen):
+            MODULE.verify_traces(self.verification_args(), "public", "secret", 
[target])
+        query = 
urllib.parse.parse_qs(urllib.parse.urlparse(requests[-1].full_url).query)
+        self.assertEqual(query["fromStartTime"], ["2026-08-31T23:59:59+00:00"])
+        self.assertEqual(query["toStartTime"], ["2026-09-01T02:00:01+00:00"])
+        self.assertEqual(target["result"]["verified"]["read_source"], 
"v2_observations")
+
+    def 
test_subagent_trace_detail_fallback_still_requires_all_exported_ids(self):
+        for complete in (False, True):
+            with self.subTest(complete=complete):
+                target, rows, _ = self.verification_fixture(subagent=True)
+                with mock.patch.object(
+                    MODULE, "fetch_trace",
+                    return_value={"observations": rows if complete else 
rows[:1]},
+                ), mock.patch.object(
+                    MODULE, "fetch_observations_legacy", 
side_effect=RuntimeError("unavailable")
+                ), mock.patch.object(
+                    MODULE, "fetch_observations_v2", 
side_effect=RuntimeError("unavailable")
+                ):
+                    if complete:
+                        MODULE.verify_traces(self.verification_args(), 
"public", "secret", [target])
+                        self.assertIn("verified", target["result"])
+                    else:
+                        with self.assertRaisesRegex(RuntimeError, "subagent"):
+                            MODULE.verify_traces(
+                                self.verification_args(verify_attempts=1), 
"public", "secret", [target]
+                            )
+
+    def test_verifies_subagent_with_more_than_ten_legacy_pages(self):
+        target, rows, _ = self.verification_fixture(subagent=True, count=1001)
+        rows = list(reversed(rows))  # The oldest root is on page 11.
+        requests = []
+
+        def fake_urlopen(request, timeout):
+            query = 
urllib.parse.parse_qs(urllib.parse.urlparse(request.full_url).query)
+            page = int(query["page"][0])
+            requests.append(page)
+            return JsonResponse({"data": rows[(page - 1) * 100:page * 100]})
+
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
+        ):
+            MODULE.verify_traces(self.verification_args(), "public", "secret", 
[target])
+        self.assertEqual(requests, list(range(1, 12)))
+        
self.assertEqual(target["result"]["verified"]["unique_observation_count"], 1001)
+
+    def test_shared_deadline_bounds_all_traces_and_http_fallbacks(self):
+        targets = [self.verification_fixture()[0]]
+        targets.extend(self.verification_fixture(subagent=True)[0] for _ in 
range(100))
+        for index, target in enumerate(targets):
+            target["result"]["trace_id"] = f"trace-{index}"
+        now = [0]
+        timeouts = []
+
+        def fake_urlopen(request, timeout):
+            timeouts.append(timeout)
+            now[0] += timeout
+            raise TimeoutError("slow read")
+
+        with mock.patch.object(MODULE.time, "monotonic", side_effect=lambda: 
now[0]), mock.patch.object(
+            MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
+        ), mock.patch.object(MODULE.time, "sleep") as sleep:
+            with self.assertRaisesRegex(RuntimeError, "trace-100"):
+                MODULE.verify_traces(
+                    self.verification_args(verify_timeout_seconds=35), 
"public", "secret", targets
+                )
+        self.assertEqual(timeouts, [30, 5])
+        self.assertEqual(now[0], 35)
+        sleep.assert_not_called()
+        
self.assertEqual(targets[-1]["result"]["verification_diagnostic"]["read_source"],
 "not_polled")
+
+    def test_shared_deadline_clips_sleep_and_stops_before_next_round(self):
+        targets = [self.verification_fixture()[0], 
self.verification_fixture(subagent=True)[0]]
+        now = [0]
+
+        def fake_sleep(seconds):
+            now[0] += seconds
+
+        with mock.patch.object(MODULE.time, "monotonic", side_effect=lambda: 
now[0]), mock.patch.object(
+            MODULE, "fetch_trace", return_value={}
+        ), mock.patch.object(
+            MODULE, "fetch_observations_legacy", return_value={"data": []}
+        ) as fetch, mock.patch.object(MODULE.time, "sleep", 
side_effect=fake_sleep) as sleep:
+            with self.assertRaises(RuntimeError):
+                MODULE.verify_traces(
+                    self.verification_args(verify_timeout_seconds=2), 
"public", "secret", targets
+                )
+        self.assertEqual(fetch.call_count, 2)
+        sleep.assert_called_once_with(2)
+
+    def test_pagination_checks_remaining_deadline_before_every_request(self):
+        for reader in (MODULE.fetch_observations_legacy, 
MODULE.fetch_observations_v2):
+            with self.subTest(reader=reader.__name__):
+                now = [0]
+                timeouts = []
+
+                def fake_urlopen(request, timeout):
+                    timeouts.append(timeout)
+                    now[0] += 3
+                    return JsonResponse({"data": [{}] * 100, "meta": 
{"cursor": "next"}})
+
+                with mock.patch.object(
+                    MODULE.time, "monotonic", side_effect=lambda: now[0]
+                ), mock.patch.object(MODULE.urllib.request, "urlopen", 
side_effect=fake_urlopen):
+                    with self.assertRaisesRegex(TimeoutError, "deadline 
exhausted"):
+                        reader("https://litefuse.example";, "public", "secret", 
"trace", deadline=5)
+                self.assertEqual(timeouts, [5, 2])
+
+    def 
test_main_exports_and_verifies_all_traces_with_failure_diagnostics(self):
+        for mode in ("success", "missing_subagent", "main_only", "no_verify", 
"dry_run"):
+            with self.subTest(mode=mode):
+                argv = [
+                    "exporter", "--input-file", "prompt", "--events-file", 
"events",
+                    "--session-id", "run-test", "--verify-attempts", "1",
+                    "--verify-sleep-seconds", "0",
+                ]
+                if mode != "main_only":
+                    argv.extend(["--subagent-sessions-dir", "/unused"])
+                if mode != "no_verify":
+                    argv.append("--verify")
+                if mode == "dry_run":
+                    argv.append("--dry-run")
+                with mock.patch.object(sys, "argv", argv):
+                    args = MODULE.parse_args()
+                events = [
+                    {
+                        "type": "item.completed", "_line_number": index + 1,
+                        "item": {"type": "agent_message", "text": text, "id": 
str(index)},
+                    }
+                    for index, text in enumerate(("reviewing", "done"))
+                ]
+                session_events = [
+                    {"type": "session_meta", "payload": {"id": 
"child-thread"}},
+                    {
+                        "type": "response_item",
+                        "payload": {"type": "message", "role": "assistant", 
"content": "done"},
+                    },
+                ]
+                child = MODULE.build_subagent_session_payload(args, 
"child.jsonl", session_events)
+                stored_rows = {}
+                requests = []
+
+                def fake_urlopen(request, timeout):
+                    requests.append(request)
+                    if request.method == "POST":
+                        payload = json.loads(request.data)
+                        for resource in payload["resourceSpans"]:
+                            for scope in resource["scopeSpans"]:
+                                for span in scope["spans"]:
+                                    attrs = attribute_values(span)
+                                    row = {"id": span["spanId"], "name": 
span["name"]}
+                                    for field in ("input", "output"):
+                                        value = 
attrs.get(f"langfuse.observation.{field}")
+                                        if value is not None:
+                                            row[field] = json.loads(value)
+                                    stored_rows.setdefault(span["traceId"], 
[]).append(row)
+                        return FakeResponse()
+                    if "/api/public/traces/" in request.full_url:
+                        return JsonResponse({})
+                    query = 
urllib.parse.parse_qs(urllib.parse.urlparse(request.full_url).query)
+                    trace_id = query["traceId"][0]
+                    rows = stored_rows[trace_id]
+                    if mode == "missing_subagent" and trace_id == 
child["trace_id"]:
+                        rows = rows[:-1]
+                    return JsonResponse({"data": rows})
+
+                stdout = io.StringIO()
+                with mock.patch.object(MODULE, "parse_args", 
return_value=args), mock.patch.object(
+                    MODULE, "read_text", return_value="review task"
+                ), mock.patch.object(MODULE, "load_jsonl", 
return_value=events), mock.patch.object(
+                    MODULE, "build_subagent_session_payloads", 
return_value=[child]
+                ) as build_children, mock.patch.object(
+                    MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
+                ), mock.patch.dict(
+                    MODULE.os.environ,
+                    {} if mode == "dry_run" else {"LANGFUSE_PUBLIC_KEY": 
"public", "LANGFUSE_SECRET_KEY": "secret"},
+                    clear=True,
+                ), redirect_stdout(stdout):
+                    if mode == "missing_subagent":
+                        with self.assertRaisesRegex(RuntimeError, 
child["trace_id"]):
+                            MODULE.main()
+                    else:
+                        MODULE.main()
+                result = json.loads(stdout.getvalue())
+                if mode == "dry_run":
+                    self.assertEqual(requests, [])
+                    self.assertEqual(result["subagent_trace_count"], 1)
+                elif mode == "no_verify":
+                    self.assertTrue(requests)
+                    self.assertTrue(all(request.method == "POST" for request 
in requests))
+                    self.assertNotIn("verified", result)
+                    self.assertNotIn("verified", result["subagent_traces"][0])
+                else:
+                    
self.assertEqual(result["verified"]["agent_message_with_turn_input_count"], 1)
+                    
self.assertEqual(result["verified"]["agent_message_with_previous_count"], 1)
+                    if mode == "main_only":
+                        build_children.assert_not_called()
+                        self.assertEqual(result["subagent_traces"], [])
+                    elif mode == "missing_subagent":
+                        self.assertIn("verification_error", result)
+                        diagnostic = 
result["subagent_traces"][0]["verification_diagnostic"]
+                        
self.assertEqual(diagnostic["missing_expected_observation_count"], 1)
+                        self.assertNotIn("verified", 
result["subagent_traces"][0])
+                    else:
+                        
self.assertEqual(result["subagent_traces"][0]["verified"]["observation_count"], 
3)
+
+    def test_main_still_rejects_missing_agent_message_context(self):
+        target, rows, _ = self.verification_fixture()
+        rows[-1]["name"] = "codex.agent_message"
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy", return_value={"data": rows}
+        ):
+            with self.assertRaisesRegex(RuntimeError, 
'"agent_message_all_have_context_window": false'):
+                MODULE.verify_traces(
+                    self.verification_args(verify_attempts=1), "public", 
"secret", [target]
+                )
+
     def test_verify_complete_main_trace_uses_legacy_observations(self):
         args = mock.Mock(
             base_url="https://litefuse.example";,
             verify_attempts=1,
+            verify_timeout_seconds=120,
             verify_sleep_seconds=0,
             min_observations=3,
             min_step_observations=1,
@@ -726,7 +1079,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
             MODULE, "fetch_observations_legacy", return_value={"data": 
observations}
         ), mock.patch.object(MODULE, "fetch_observations_v2") as v2_fetch:
-            result = MODULE.verify_trace(
+            result = self.verify_single_trace(
                 args, "public", "secret", "trace-id", len(observations)
             )
 
@@ -738,6 +1091,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         args = mock.Mock(
             base_url="https://litefuse.example";,
             verify_attempts=1,
+            verify_timeout_seconds=120,
             verify_sleep_seconds=0,
             min_observations=1,
             min_step_observations=1,
@@ -763,7 +1117,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
             with self.assertRaisesRegex(
                 RuntimeError, '"required_observation_count": 3'
             ):
-                MODULE.verify_trace(
+                self.verify_single_trace(
                     args, "public", "secret", "trace-id", 3
                 )
 
@@ -771,6 +1125,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         args = mock.Mock(
             base_url="https://litefuse.example";,
             verify_attempts=1,
+            verify_timeout_seconds=120,
             verify_sleep_seconds=0,
             min_observations=1,
             min_step_observations=1,
@@ -802,7 +1157,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
             with self.assertRaisesRegex(
                 RuntimeError, '"duplicate_observation_count": 1'
             ):
-                MODULE.verify_trace(
+                self.verify_single_trace(
                     args, "public", "secret", "trace-id", 2
                 )
 
@@ -810,6 +1165,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         args = SimpleNamespace(
             base_url="https://litefuse.example";,
             verify_attempts=1,
+            verify_timeout_seconds=120,
             verify_sleep_seconds=0,
             min_observations=1,
             min_step_observations=1,
@@ -849,7 +1205,7 @@ class LitefuseOtelExporterTest(unittest.TestCase):
                     with self.assertRaisesRegex(
                         RuntimeError, '"observations_missing_id_count": 1'
                     ):
-                        MODULE.verify_trace(args, "public", "secret", 
"trace-id", 2)
+                        self.verify_single_trace(args, "public", "secret", 
"trace-id", 2)
 
 
 if __name__ == "__main__":
diff --git a/.github/workflows/code-review-runner.yml 
b/.github/workflows/code-review-runner.yml
index 4e8cbbdfb9c..196025cd0e0 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -949,6 +949,7 @@ jobs:
             --min-step-observations 2 \
             --verify-attempts 24 \
             --verify-sleep-seconds 5 \
+            --verify-timeout-seconds 120 \
             --verify
 
       - name: Sync Codex sessions back to OSS


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to