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
5.6 KiB
5.6 KiB
Control-plane DB substrate (#613)
Status: Implemented (SQLite single-writer MVP)
ADR: 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)
- Assignable
work_items.kind∈ {issue,pr} only — never raw Sentry/GlitchTip incidents. - Two concurrent sessions cannot both receive an active assignment on the same open work item (second gets
wait). - Merged/closed work items return
no_safe_workat assign time, andrequire_valid_assignmentfails closed if the work item becomes terminal later. - Assignments pin
expected_head_sha; mutations fail closed if the work item head drifts. - 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). incident_linksoptional scope fields are stored as empty strings (never NULL) so UNIQUE is canonical across minimal upserts.- Legacy
incident_linksmigration 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
#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_linkshere 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=falsereturns a dry-run selection (outcome=preview) with skip reasons.apply=truereserves viaControlPlaneDB.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
- Control-plane DB is the coordination authority — file locks / comment-only leases are not authoritative alone.
- Active foreign leases cannot be stolen; inspect returns
wait_foreign_active. - Abandon requires
(dead_process OR missing_worktree)andno_live_mutation_risk; foreign abandon also needsoperator_authorizedor full dead+missing+no_open_pr proof. - Adopt provenance always records
adopted_from_session_id,adopted_by_session_id, work identity, optional head SHA, and worktree path. - Reviewer→merger comment handoff (
gitea_adopt_merger_pr_lease) is unchanged and remains the Gitea-thread durability path.
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_JSONorGITEA_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
python3 -m pytest tests/test_control_plane_db.py -q