mvanhorn/last30days-skill · error · HandoffContractError

Could not read {label.lower()} {path}: {exc}

Error message

Could not read {label.lower()} {path}: {exc}

What it means

`_parse_handoff_envelope` (shared by the nominations-bundle and pending-report readers) raises HandoffContractError when `path.read_text` raises OSError — the handoff file exists but cannot be read (permissions, I/O error, race where it was deleted between the exists() check and the open). The message names the file kind ('nominations bundle' / 'pending discovery report'), the path, and the OS error; the CLI maps it to exit 2.

Source

Thrown at skills/last30days/scripts/lib/discovery_handoff.py:398

def _parse_handoff_envelope(
    path: Path,
    *,
    label: str,
    kind: str,
    schema_version: str,
    remedy: str,
    missing_id_context: str,
    stale_context: str,
) -> tuple[dict[str, Any], str, Any]:
    """Shared strict top-level validation for the two engine-written handoff
    files (nominations bundle, pending report): readable, valid JSON object,
    right kind and schema version, bundle_id present, within TTL. Returns
    (payload, bundle_id, generated_at)."""
    try:
        raw = path.read_text(encoding="utf-8")
    except OSError as exc:
        raise HandoffContractError(
            f"Could not read {label.lower()} {path}: {exc}"
        ) from exc
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise HandoffContractError(
            f"{label} {path} is not valid JSON: {exc}"
        ) from exc
    if not isinstance(payload, dict):
        raise HandoffContractError(
            f"{label} {path} must be a top-level JSON object, "
            f"got {type(payload).__name__}."
        )
    version = payload.get("schema_version")
    if version != schema_version:
        raise HandoffContractError(
            f"{label} {path} has schema version {version!r}; this "
            f"build reads {schema_version!r}. {remedy}"

View on GitHub (pinned to c7460f6114)

Solutions

  1. Inspect the exact path in the message: `ls -l <path>` for mode/owner and `cat <path>` to reproduce the read failure.
  2. Fix permissions (`chmod 644` / `chown`) or remount the volume read-write.
  3. Re-run the leg that produces the file (nominate-only sweep, or resume leg for the pending report) to rewrite it cleanly.
  4. Serialize concurrent discovery runs on the same save dir — the handoff files are single-writer state.

Example fix

# before: bundle written under sudo, unreadable as user
# after
sudo chown "$(id -un):$(id -gn)" /path/to/save-dir/discover-nominations.json && python3 last30days.py "topic" --discover --judgments judgments.json --save-dir /path/to/save-dir
Defensive patterns

Strategy: try-catch

Validate before calling

from pathlib import Path

def readable_file(path: Path) -> bool:
    try:
        with path.open("r", encoding="utf-8"):
            return True
    except OSError:
        return False

Try / catch

from lib import discovery_handoff
try:
    bundle = discovery_handoff.load_nominations_bundle(save_dir=save_dir, config_dir=config_dir)
except discovery_handoff.HandoffContractError as exc:
    if "Could not read" in exc.message:
        # permissions / IO: fix the store, then retry the leg
        ...
    raise

Prevention

When it happens

Trigger: `discover-nominations.json` or `discover-pending.json` present but mode 000 / owned by another user; a network mount dropping mid-read; the file deleted by a concurrent process after discovery but before parse.

Common situations: Root-created files from earlier sudo runs; NFS/FUSE latency or auth expiry on the save dir; two concurrent engine invocations where one cleans state while the other reads; container read-only volume mounts.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/a2c5781f905953bf. Report an issue: GitHub.