mvanhorn/last30days-skill · error · HandoffContractError

{label} {path} is stale (generated_at={generated_at!r}, TTL

Error message

{label} {path} is stale (generated_at={generated_at!r}, TTL {int(DISCOVERY_HANDOFF_TTL_SECONDS)}s): {stale_context}. {remedy}

What it means

`_parse_handoff_envelope` raises HandoffContractError when `generated_at` fails `env.is_timestamp_fresh(generated_at, DISCOVERY_HANDOFF_TTL_SECONDS)` with TTL fixed at 3600 seconds (1 hour) — a module constant deliberately NOT overridable via LAST30DAYS_REPORT_CACHE_TTL_SECONDS so shortening the report-cache TTL cannot shrink the host's judgment-authoring window. Staleness guards against resuming a discovery protocol whose data window has moved on; the remedy differs per artifact (re-sweep for the bundle, resume leg for the pending report).

Source

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

        raise HandoffContractError(
            f"{label} {path} has schema version {version!r}; this "
            f"build reads {schema_version!r}. {remedy}"
        )
    file_kind = payload.get("kind")
    if file_kind != kind:
        raise HandoffContractError(
            f"{label} {path} has kind {file_kind!r}; expected "
            f"{kind!r}. {remedy}"
        )
    bundle_id = str(payload.get("bundle_id") or "")
    if not bundle_id:
        raise HandoffContractError(
            f"{label} {path} is missing its bundle_id; "
            f"{missing_id_context}. {remedy}"
        )
    generated_at = payload.get("generated_at")
    if not env.is_timestamp_fresh(generated_at, DISCOVERY_HANDOFF_TTL_SECONDS):
        raise HandoffContractError(
            f"{label} {path} is stale (generated_at="
            f"{generated_at!r}, TTL {int(DISCOVERY_HANDOFF_TTL_SECONDS)}s): "
            f"{stale_context}. {remedy}"
        )
    return payload, bundle_id, generated_at


def _parse_bundle_file(path: Path) -> NominationsBundle:
    payload, bundle_id, generated_at = _parse_handoff_envelope(
        path,
        label="Nominations bundle",
        kind=schema.DISCOVERY_NOMINATIONS_KIND,
        schema_version=schema.DISCOVERY_NOMINATIONS_SCHEMA_VERSION,
        remedy=_RESWEEP_REMEDY,
        missing_id_context="judgments cannot bind to it",
        stale_context="the momentum window it captured has moved on",
    )
    version = payload.get("schema_version")

View on GitHub (pinned to c7460f6114)

Solutions

  1. Complete each protocol leg within 1 hour of the previous leg's write; if authoring will take longer, plan to re-run `--discover --nominate-only` and judge against the fresh bundle.
  2. For a stale bundle: re-sweep (`--discover --nominate-only`), then re-author judgments bound to the new bundle_id.
  3. For a stale pending report: re-run the resume leg (`--discover --judgments <file>`) if the bundle is still fresh; only restart the whole protocol when the bundle is stale too (the message's stale_context and remedy encode this).
  4. Check system clock (`date -u`; generated_at is UTC) — a skewed clock makes valid files read stale.

Example fix

# before: bundle generated 09:00, judgments attempted 11:00
# after: re-sweep inside the window, then judge within an hour
python3 last30days.py "topic" --discover --nominate-only --save-dir /tmp/run && python3 last30days.py "topic" --discover --judgments judgments.json --save-dir /tmp/run
Defensive patterns

Strategy: validation

Validate before calling

import json, time
from pathlib import Path
from lib.env import is_timestamp_fresh
from lib.discovery_handoff import DISCOVERY_HANDOFF_TTL_SECONDS

def bundle_is_fresh(path: Path) -> bool:
    try:
        ts = json.loads(path.read_text(encoding="utf-8")).get("generated_at")
        return is_timestamp_fresh(ts, DISCOVERY_HANDOFF_TTL_SECONDS)  # 3600s window
    except (OSError, json.JSONDecodeError):
        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 "is stale" in exc.message:
        # budget exceeded: re-run the producing leg within a new 1h window
        ...

Prevention

When it happens

Trigger: Running leg 2 more than 1 hour after leg 1 finished; authoring judgments slowly and exceeding the window; leaving a pending report overnight before leg 3; a machine whose clock skewed forward relative to generated_at.

Common situations: Human- or model-paced judgment authoring taking over an hour; pausing a session mid-protocol; resuming the next day; timezone/clock drift on VMs making fresh files read stale.

Related errors


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