Make active leases queryable workflow state with canonical adopt, release, expire/reclaim, and abandon operations on the #613 control-plane DB substrate. - lease_lifecycle.py: policy, proof gates, safe_next_action vocabulary - control_plane_db: schema v3 provenance columns + list/inspect/adopt/abandon - MCP tools: gitea_*_workflow_lease(s) for list/inspect/adopt/release/expire/abandon/reclaim - issue_lock_store: sanctioned reclaim of expired author locks (dead pid / missing wt) - Tests cover lifecycle, foreign steal refusal, allocator compatibility, merger handoff
106 lines
5.6 KiB
Markdown
106 lines
5.6 KiB
Markdown
# Control-plane DB substrate (#613)
|
|
|
|
**Status:** Implemented (SQLite single-writer MVP)
|
|
|
|
**ADR:** [`mcp-allocator-control-plane-observability-adr.md`](mcp-allocator-control-plane-observability-adr.md)
|
|
|
|
**Module:** `control_plane_db.py`
|
|
|
|
## Architecture statement
|
|
|
|
> **DB coordinates, Gitea records, Sentry/GlitchTip observe; the bridge is the only path that turns observations into Gitea work.**
|
|
|
|
## What this ships
|
|
|
|
| Capability | Notes |
|
|
|------------|--------|
|
|
| Schema | `sessions`, `work_items`, `leases`, `assignments`, `terminal_locks`, `events`, `incident_links` |
|
|
| Atomic assign+lease | `ControlPlaneDB.assign_and_lease` — one `BEGIN IMMEDIATE` transaction |
|
|
| Mutation gate | `require_valid_assignment` — live lease + allowed action + non-terminal work + non-stale head |
|
|
| Heartbeat / release / expire | Lease lifecycle helpers |
|
|
| Terminal-lock index | Routing signal for #600 (terminal path first) |
|
|
| `incident_links` | Provider-neutral link model for #612 — **not** assignable work; scope keys NULL-safe |
|
|
|
|
## Hard rules (enforced in code)
|
|
|
|
1. Assignable `work_items.kind` ∈ {`issue`, `pr`} only — **never** raw Sentry/GlitchTip incidents.
|
|
2. Two concurrent sessions cannot both receive an active assignment on the same open work item (second gets `wait`).
|
|
3. Merged/closed work items return `no_safe_work` at assign time, and `require_valid_assignment` fails closed if the work item becomes terminal later.
|
|
4. Assignments pin `expected_head_sha`; mutations fail closed if the work item head drifts.
|
|
5. SQLite path is the **single-writer MVP** (`GITEA_CONTROL_PLANE_DB`, default under `~/.cache/gitea-tools/control-plane/`). Multi-session multi-host production requires **Postgres** or a **single allocator daemon** (ADR §6).
|
|
6. `incident_links` optional scope fields are stored as empty strings (never NULL) so UNIQUE is canonical across minimal upserts.
|
|
7. Legacy `incident_links` migration collapses NULL-scope duplicates **only** when Gitea targets **and** all meaningful observation metadata agree (fingerprint, status, event_count, permalink, timestamps, linked PRs, etc.). Conflicting metadata fails closed — no silent discard.
|
|
|
|
## Dependency chain
|
|
|
|
```text
|
|
#613 control-plane DB (this) → #600 allocator API → #612 incident bridge
|
|
```
|
|
|
|
- **#600** must call this substrate (not file locks / comment-only leases alone) for completion.
|
|
- **#612** must write `incident_links` here and create **Gitea issues**; the allocator assigns those issues, not raw incidents.
|
|
|
|
## Allocator API (#600)
|
|
|
|
Module: `allocator_service.py` · MCP tool: `gitea_allocate_next_work`
|
|
|
|
- Workers call `gitea_allocate_next_work(apply=false|true)` instead of self-selecting work.
|
|
- `apply=false` returns a dry-run selection (`outcome=preview`) with skip reasons.
|
|
- `apply=true` reserves via `ControlPlaneDB.assign_and_lease` (atomic assignment+lease).
|
|
- Coordination source is **always** the control-plane DB — not file locks or comment-only leases.
|
|
- Routing follows ADR §5.3 (REQUEST_CHANGES → author, terminal path first, foreign lease → wait).
|
|
- Raw monitoring incidents are never candidates.
|
|
|
|
|
|
## Lease lifecycle API (#601)
|
|
|
|
Module: `lease_lifecycle.py` · MCP tools: `gitea_*_workflow_lease(s)`
|
|
|
|
Active control-plane leases are **first-class workflow state**:
|
|
|
|
| Tool | Purpose |
|
|
|------|---------|
|
|
| `gitea_list_workflow_leases` | List active (or all) leases for a repo |
|
|
| `gitea_inspect_workflow_lease` | Freshness + `safe_next_action` for one lease id |
|
|
| `gitea_adopt_workflow_lease` | Owner-resume or sanctioned reclaim with provenance |
|
|
| `gitea_release_workflow_lease` | Explicit owner release (audited) |
|
|
| `gitea_expire_workflow_leases` | Deterministic expire of past-`expires_at` leases |
|
|
| `gitea_abandon_workflow_lease` | Abandon with required proof |
|
|
| `gitea_reclaim_expired_workflow_lease` | Expire + re-assign with provenance |
|
|
|
|
### Hard rules
|
|
|
|
1. Control-plane DB is the coordination authority — file locks / comment-only leases are not authoritative alone.
|
|
2. Active foreign leases cannot be stolen; inspect returns `wait_foreign_active`.
|
|
3. Abandon requires `(dead_process OR missing_worktree)` and `no_live_mutation_risk`; foreign abandon also needs `operator_authorized` or full dead+missing+no_open_pr proof.
|
|
4. Adopt provenance always records `adopted_from_session_id`, `adopted_by_session_id`, work identity, optional head SHA, and worktree path.
|
|
5. Reviewer→merger comment handoff (`gitea_adopt_merger_pr_lease`) is unchanged and remains the Gitea-thread durability path.
|
|
|
|
```bash
|
|
python3 -m pytest tests/test_lease_lifecycle.py tests/test_control_plane_db.py tests/test_allocator_service.py tests/test_merger_lease_adoption.py -q
|
|
```
|
|
|
|
## Incident bridge (#612)
|
|
|
|
Module: `incident_bridge.py` · MCP tools: `gitea_observability_*`
|
|
|
|
- Bridge converts Sentry/GlitchTip observations → **normal Gitea issues** + `incident_links`.
|
|
- Phase-1: `gitea_observability_reconcile_incident(apply=false|true)` with JSON observation.
|
|
- Dry-run performs **no** Gitea mutation and **no** DB write.
|
|
- Apply reuses existing links or creates one Gitea issue; never invents `work_items.kind=incident`.
|
|
- Config: `GITEA_OBSERVABILITY_PROJECTS_JSON` or `GITEA_OBSERVABILITY_PROJECTS_FILE`.
|
|
- Provider tokens never appear in issue bodies, links, or tool results.
|
|
- Allocator sees bridge work only after a Gitea issue exists.
|
|
|
|
## Non-goals (intentionally deferred)
|
|
|
|
- Full unsupervised watchdog auto-filing (prefer explicit reconcile first)
|
|
- Assuming GlitchTip writeback API equals Sentry without verification
|
|
- Gitea comment/label mirror writers for every assignment (optional later)
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
python3 -m pytest tests/test_control_plane_db.py -q
|
|
```
|