← Files LegalQuants LitigationARCHIVED FILE

skills/docreview/scripts/shared/run_codex_finding_worker.py

14.8 KB · Oct 5, 2026 · 18:33 UTC

↓ Download file

#!/usr/bin/env python3
"""Run one compact finding assignment through an isolated Codex context."""

from __future__ import annotations

import argparse
import hashlib
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

DEFAULT_ISSUE_BATCH_SIZE = 12
MAX_ISSUES_PER_CALL = 12
LOW_EFFORTS = {"none", "minimal", "low"}


def load_object(path: Path, label: str) -> dict[str, Any]:
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        raise OSError(f"cannot read {label}: {error}") from error
    if not isinstance(value, dict):
        raise OSError(f"{label} must be an object")
    return value


def usage_from_events(stream: str) -> dict[str, int] | None:
    totals = {
        "input_tokens": 0,
        "cached_input_tokens": 0,
        "output_tokens": 0,
        "reasoning_output_tokens": 0,
    }
    completed_turns = 0
    for line in stream.splitlines():
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue
        if not isinstance(event, dict) or event.get("type") != "turn.completed":
            continue
        usage = event.get("usage")
        if not isinstance(usage, dict):
            continue
        values: dict[str, int] = {}
        for key in totals:
            value = usage.get(key, 0)
            if not isinstance(value, int) or isinstance(value, bool) or value < 0:
                break
            values[key] = value
        else:
            completed_turns += 1
            for key, value in values.items():
                totals[key] += value
    if completed_turns == 0:
        return None
    totals["model_calls"] = completed_turns
    totals["total_tokens"] = totals["input_tokens"] + totals["output_tokens"]
    return totals


def write_usage(
    path: Path,
    assignment: dict[str, Any],
    model: str,
    effort: str,
    usage: dict[str, int],
) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_name(path.name + ".tmp")
    temporary.write_text(
        json.dumps(
            {
                "contract": "codex-usage/1",
                "effort": effort,
                "job_id": assignment.get("job_id"),
                "model": model,
                **usage,
            },
            indent=2,
            sort_keys=True,
        )
        + "\n",
        encoding="utf-8",
    )
    temporary.replace(path)


def verified_images(
    assignment: dict[str, Any], sidecar_path: Path
) -> list[tuple[int, int, Path]]:
    sidecar = load_object(sidecar_path, "review-copy sidecar")
    bundle_root = sidecar.get("bundle_root")
    documents = sidecar.get("documents")
    if not isinstance(bundle_root, str) or not isinstance(documents, list):
        raise OSError("review-copy sidecar is malformed")
    bundle = (sidecar_path.parent / bundle_root).resolve()
    copies = {
        item.get("doc_id"): item
        for item in documents
        if isinstance(item, dict) and isinstance(item.get("doc_id"), str)
    }
    images: list[tuple[int, int, Path]] = []
    assigned = assignment.get("documents")
    if not isinstance(assigned, list):
        raise OSError("assignment documents must be an array")
    for ordinal, document in enumerate(assigned, 1):
        if not isinstance(document, dict) or not isinstance(document.get("id"), str):
            raise OSError("assignment contains a malformed document")
        copy = copies.get(document["id"])
        if not isinstance(copy, dict) or copy.get("status") != "ready":
            raise OSError(f"review copy is not ready for document {ordinal}")
        derivatives = copy.get("derivatives")
        if not isinstance(derivatives, list):
            raise OSError(
                f"review copy derivatives are malformed for document {ordinal}"
            )
        doc_images: list[tuple[int, int, Path]] = []
        for derivative in derivatives:
            if not isinstance(derivative, dict):
                continue
            media_type = derivative.get("media_type")
            relative = derivative.get("path")
            expected_hash = derivative.get("sha256")
            if not isinstance(media_type, str) or not media_type.startswith("image/"):
                continue
            if not isinstance(relative, str) or not isinstance(expected_hash, str):
                raise OSError(f"image derivative is malformed for document {ordinal}")
            path = (bundle / relative).resolve()
            try:
                path.relative_to(bundle)
            except ValueError as error:
                raise OSError("review-copy path escapes the bundle") from error
            if not path.is_file():
                raise OSError(f"review-copy image is missing for document {ordinal}")
            actual = "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
            if actual != expected_hash:
                raise OSError(f"review-copy image hash drifted for document {ordinal}")
            page = derivative.get("page")
            if not isinstance(page, int) or isinstance(page, bool) or page < 1:
                page = 1
            doc_images.append((ordinal, page, path))
        if document.get("readability") == "scanned" and not doc_images:
            raise OSError(f"scanned document {ordinal} has no image review copy")
        images.extend(
            sorted(doc_images, key=lambda item: (item[1], item[2].as_posix()))
        )
    return images


def batched_assignments(
    assignment: dict[str, Any], batch_size: int
) -> list[tuple[int, dict[str, Any]]]:
    if not 1 <= batch_size <= MAX_ISSUES_PER_CALL:
        raise OSError(f"issue batch size must be 1..{MAX_ISSUES_PER_CALL}")
    issues = assignment.get("issue_items")
    if (
        not isinstance(issues, list)
        or not issues
        or not all(isinstance(item, dict) for item in issues)
    ):
        raise OSError("assignment issue_items must be a non-empty object array")
    return [
        (offset, {**assignment, "issue_items": issues[offset : offset + batch_size]})
        for offset in range(0, len(issues), batch_size)
    ]


def batch_prompt(
    prompt: str,
    original_assignment: str,
    assignment: dict[str, Any],
    batch_number: int,
    batch_count: int,
) -> str:
    encoded = json.dumps(assignment, indent=2, sort_keys=True) + "\n"
    if original_assignment in prompt:
        rendered = prompt.replace(original_assignment, encoded, 1)
    else:
        rendered = prompt + "\n\n# Assignment\n\n```json\n" + encoded + "```\n"
    return (
        rendered
        + "\n\n# Bounded request batch\n\n"
        + f"This is request batch {batch_number} of {batch_count}. Review every "
        "supplied document page against every issue in this batch. Number this "
        "batch locally from 1 in the returned `n` fields; the adapter restores "
        "the approved full-lens order after validating every issue ID.\n"
    )


def validate_batch_result(
    value: object,
    assignment: dict[str, Any],
    offset: int,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    if not isinstance(value, dict):
        raise OSError("batch result must be an object")
    if value.get("contract") != "finding-worker/2":
        raise OSError("batch result has the wrong contract")
    if value.get("review_plan_id") != assignment.get("review_plan_id") or value.get(
        "job_id"
    ) != assignment.get("job_id"):
        raise OSError("batch result has drifted run identifiers")
    issues = assignment.get("issue_items")
    determinations = value.get("determinations")
    candidates = value.get("privilege_candidates")
    if not isinstance(issues, list) or not isinstance(determinations, list):
        raise OSError("batch result has no determination array")
    if len(determinations) != len(issues):
        raise OSError("batch result did not cover every batched request")
    normalized: list[dict[str, Any]] = []
    pairs = zip(determinations, issues)  # noqa: B905 - lengths checked; Python 3.9
    for local_index, (raw, issue) in enumerate(pairs, 1):
        if not isinstance(raw, dict) or not isinstance(issue, dict):
            raise OSError("batch result contains a malformed determination")
        if raw.get("n") != local_index or raw.get("issue_id") != issue.get("issue_id"):
            raise OSError("batch result changed request order or identity")
        normalized.append({**raw, "n": offset + local_index})
    if not isinstance(candidates, list) or not all(
        isinstance(candidate, dict) for candidate in candidates
    ):
        raise OSError("batch result has a malformed privilege candidate array")
    return normalized, candidates


def dump_atomic(path: Path, value: object) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_name(path.name + ".tmp")
    temporary.write_text(
        json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8"
    )
    temporary.replace(path)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--assignment", required=True)
    parser.add_argument("--output", required=True)
    parser.add_argument("--schema", required=True)
    parser.add_argument("--room-root", required=True)
    parser.add_argument("--review-copies", required=True)
    parser.add_argument("--model", required=True)
    parser.add_argument("--effort", required=True)
    parser.add_argument(
        "--issue-batch-size", type=int, default=DEFAULT_ISSUE_BATCH_SIZE
    )
    parser.add_argument("--allow-low-effort", action="store_true")
    parser.add_argument("--usage-out")
    parser.add_argument("--require-usage", action="store_true")
    parser.add_argument("--codex", default="codex")
    args = parser.parse_args()
    try:
        assignment = load_object(Path(args.assignment), "assignment")
        images = verified_images(assignment, Path(args.review_copies))
        batches = batched_assignments(assignment, args.issue_batch_size)
    except OSError as error:
        print(f"run_codex_finding_worker: {error}", file=sys.stderr)
        return 2
    if not 1 <= args.issue_batch_size <= MAX_ISSUES_PER_CALL:
        print(
            "run_codex_finding_worker: --issue-batch-size must be "
            f"1..{MAX_ISSUES_PER_CALL}",
            file=sys.stderr,
        )
        return 2
    if args.effort.lower() in LOW_EFFORTS and not args.allow_low_effort:
        print(
            "run_codex_finding_worker: substantive mapping requires at least medium "
            "reasoning effort; --allow-low-effort is reserved for an explicit "
            "user override",
            file=sys.stderr,
        )
        return 2
    prompt = sys.stdin.read()
    original_assignment = Path(args.assignment).read_text(encoding="utf-8")
    page_map = "\n".join(
        f"- Attachment {index}: Document {ordinal}, page {page}"
        for index, (ordinal, page, _) in enumerate(images, 1)
    )
    all_determinations: list[dict[str, Any]] = []
    all_candidates: list[dict[str, Any]] = []
    event_streams: list[str] = []
    with tempfile.TemporaryDirectory(prefix="lq-review-batches-") as temporary:
        temporary_root = Path(temporary)
        for batch_index, (offset, batch) in enumerate(batches, 1):
            batch_output = temporary_root / f"batch-{batch_index:04d}.json"
            rendered_prompt = batch_prompt(
                prompt, original_assignment, batch, batch_index, len(batches)
            )
            if images:
                rendered_prompt += (
                    "\n\n# Attached verified review pages\n\n"
                    + page_map
                    + "\nInspect every attachment. Use its mapped document ordinal "
                    "and page.\n"
                )
            command = [
                args.codex,
                "exec",
                "--json",
                "--ephemeral",
                "--ignore-user-config",
                "--ignore-rules",
                "--skip-git-repo-check",
                "--sandbox",
                "read-only",
                "--model",
                args.model,
                "-c",
                f'model_reasoning_effort="{args.effort}"',
                "--output-schema",
                args.schema,
                "--output-last-message",
                str(batch_output),
                "--cd",
                str(Path(args.room_root).resolve()),
            ]
            for _, _, path in images:
                command.extend(["--image", str(path)])
            command.extend(["--", "-"])
            completed = subprocess.run(
                command,
                input=rendered_prompt,
                text=True,
                check=False,
                capture_output=True,
            )
            event_streams.append(completed.stdout)
            if completed.stdout:
                print(completed.stdout, end="")
            if completed.stderr:
                print(completed.stderr, end="", file=sys.stderr)
            if completed.returncode != 0:
                return completed.returncode
            try:
                raw_batch = load_object(batch_output, f"batch {batch_index} result")
                determinations, candidates = validate_batch_result(
                    raw_batch, batch, offset
                )
            except OSError as error:
                print(f"run_codex_finding_worker: {error}", file=sys.stderr)
                if batch_output.exists():
                    Path(args.output).parent.mkdir(parents=True, exist_ok=True)
                    shutil.copyfile(batch_output, args.output)
                    break
                return 3
            all_determinations.extend(determinations)
            all_candidates.extend(candidates)
        else:
            unique_candidates = {
                json.dumps(candidate, sort_keys=True, separators=(",", ":")): candidate
                for candidate in all_candidates
            }
            dump_atomic(
                Path(args.output),
                {
                    "contract": "finding-worker/2",
                    "determinations": all_determinations,
                    "job_id": assignment.get("job_id"),
                    "privilege_candidates": [
                        unique_candidates[key] for key in sorted(unique_candidates)
                    ],
                    "review_plan_id": assignment.get("review_plan_id"),
                },
            )
    usage = usage_from_events("\n".join(event_streams))
    if usage is not None and args.usage_out:
        write_usage(Path(args.usage_out), assignment, args.model, args.effort, usage)
    if completed.returncode == 0 and args.require_usage and usage is None:
        print(
            "run_codex_finding_worker: completed without token telemetry",
            file=sys.stderr,
        )
        return 3
    return completed.returncode


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 46777306dc187de1efa07e043ec34b6103069e4fdc438047fe807b8985a122ef