{"record":{"id":"834b4fb15b5b367e","repo":"mvanhorn/last30days-skill","slug":"label-path-is-stale-generated-at-generated-a","errorCode":null,"errorMessage":"{label} {path} is stale (generated_at={generated_at!r}, TTL {int(DISCOVERY_HANDOFF_TTL_SECONDS)}s): {stale_context}. {remedy}","messagePattern":"(.+?) (.+?) is stale \\(generated_at=(.+?), TTL (.+?)s\\): (.+?)\\. (.+?)","errorType":"exception","errorClass":"HandoffContractError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/discovery_handoff.py","lineNumber":432,"sourceCode":"        raise HandoffContractError(\n            f\"{label} {path} has schema version {version!r}; this \"\n            f\"build reads {schema_version!r}. {remedy}\"\n        )\n    file_kind = payload.get(\"kind\")\n    if file_kind != kind:\n        raise HandoffContractError(\n            f\"{label} {path} has kind {file_kind!r}; expected \"\n            f\"{kind!r}. {remedy}\"\n        )\n    bundle_id = str(payload.get(\"bundle_id\") or \"\")\n    if not bundle_id:\n        raise HandoffContractError(\n            f\"{label} {path} is missing its bundle_id; \"\n            f\"{missing_id_context}. {remedy}\"\n        )\n    generated_at = payload.get(\"generated_at\")\n    if not env.is_timestamp_fresh(generated_at, DISCOVERY_HANDOFF_TTL_SECONDS):\n        raise HandoffContractError(\n            f\"{label} {path} is stale (generated_at=\"\n            f\"{generated_at!r}, TTL {int(DISCOVERY_HANDOFF_TTL_SECONDS)}s): \"\n            f\"{stale_context}. {remedy}\"\n        )\n    return payload, bundle_id, generated_at\n\n\ndef _parse_bundle_file(path: Path) -> NominationsBundle:\n    payload, bundle_id, generated_at = _parse_handoff_envelope(\n        path,\n        label=\"Nominations bundle\",\n        kind=schema.DISCOVERY_NOMINATIONS_KIND,\n        schema_version=schema.DISCOVERY_NOMINATIONS_SCHEMA_VERSION,\n        remedy=_RESWEEP_REMEDY,\n        missing_id_context=\"judgments cannot bind to it\",\n        stale_context=\"the momentum window it captured has moved on\",\n    )\n    version = payload.get(\"schema_version\")","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/discovery_handoff.py#L414-L450","documentation":"`_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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: bundle generated 09:00, judgments attempted 11:00\n# after: re-sweep inside the window, then judge within an hour\npython3 last30days.py \"topic\" --discover --nominate-only --save-dir /tmp/run && python3 last30days.py \"topic\" --discover --judgments judgments.json --save-dir /tmp/run","handlingStrategy":"validation","validationCode":"import json, time\nfrom pathlib import Path\nfrom lib.env import is_timestamp_fresh\nfrom lib.discovery_handoff import DISCOVERY_HANDOFF_TTL_SECONDS\n\ndef bundle_is_fresh(path: Path) -> bool:\n    try:\n        ts = json.loads(path.read_text(encoding=\"utf-8\")).get(\"generated_at\")\n        return is_timestamp_fresh(ts, DISCOVERY_HANDOFF_TTL_SECONDS)  # 3600s window\n    except (OSError, json.JSONDecodeError):\n        return False","typeGuard":null,"tryCatchPattern":"from lib import discovery_handoff\ntry:\n    bundle = discovery_handoff.load_nominations_bundle(save_dir=save_dir, config_dir=config_dir)\nexcept discovery_handoff.HandoffContractError as exc:\n    if \"is stale\" in exc.message:\n        # budget exceeded: re-run the producing leg within a new 1h window\n        ...","preventionTips":["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."],"tags":["discovery","handoff","ttl","staleness","exit-code-2"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}