feat(ops-runs): expire claims that keep losing their lease

Lapsed claims were reopened with no attempt ceiling, so a run whose worker
lost its lease (including a 409 before dispatch) could cycle forever.
Activity Core now reopens a lapsed claim below OPS_RUN_MAX_ATTEMPTS and sets
terminal 'expired' at the ceiling. The owner is kept for audit, and
/ops-runs/expire-leases reports reopened and expired counts. Answers the
REINAH-WP-0003-T05 open point on thread 27cd4c47 (ACTIVITY-WP-ADHOC-2026-10-19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 244572@bnt-lap001
Assistant-Session: 3b235e1a-ae45-4d0f-ae70-17a9bd78ece8
This commit is contained in:
tegwick 2026-10-19 15:30:29 +02:00
parent 3b25aea7b0
commit 42347da012
6 changed files with 192 additions and 14 deletions

View file

@ -61,7 +61,14 @@ workplan task file. Not an issue-core or Forgejo ticket.
- `labels_mode`: `any` (default) — run must contain at least one listed label;
`all` — run must contain every listed label; omit `labels` to claim any open run.
- Claim uses `FOR UPDATE SKIP LOCKED` for concurrency safety.
- Stale claims (`state=claimed` and `lease_until <= now()`) are reopened before select.
- Stale claims (`state=claimed` and `lease_until <= now()`) are recovered by
Activity Core before every claim select and by `POST /ops-runs/expire-leases`
(returns `{"reopened": n, "expired": m}`). Below `OPS_RUN_MAX_ATTEMPTS` the row
returns to `open` with owner and lease cleared; at the ceiling it becomes
terminal `expired` (owner kept for audit, `result.error` set). A worker whose
lease was lost, including one that got a 409 before dispatch, must not close
the row; it leaves recovery to this expiry path. A late close on an expired
row is refused as `state_conflict`.
- Heartbeat, complete, and fail lock the row and require an active lease
(`lease_until > now()`). An expired worker cannot revive or close its claim.
- A first terminal close returns `close_disposition: applied`. If its response