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
- 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.
- For a stale bundle: re-sweep (`--discover --nominate-only`), then re-author judgments bound to the new bundle_id.
- 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).
- 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
- Treat the 1-hour handoff TTL as a hard deadline for authoring judgments and finalizing angles.
- If authoring will take longer, plan for a fresh nominate-only sweep instead of racing the TTL.
- Keep system clocks NTP-synced; generated_at is compared as UTC.
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
- No handoff location available to write the nominations bundl
- Could not write nominations bundle {path}: {exc}
- No discovery nominations bundle found. Searched:\n{_searched
- Could not read {label.lower()} {path}: {exc}
- {label} {path} is not valid JSON: {exc}
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/834b4fb15b5b367e.
Report an issue: GitHub.