Replace single global issue-lock file with repo/session-safe lock ownership #443

Closed
opened 2026-07-07 12:48:11 -05:00 by jcwalker3 · 1 comment
Owner

Problem

The MCP issue-lock mechanism uses one shared global file:

/private/tmp/gitea_issue_lock.json

(ISSUE_LOCK_FILE = "/tmp/gitea_issue_lock.json" in gitea_mcp_server.py; same path on macOS via /private/tmp.)

That file is shared across every session, user, repo, issue, and role. Concurrent sessions overwrite each other's active locks. This caused or exposed the #420 PR creation deadlock and creates cross-session interference risk.

Observed evidence

A recovery attempt for Gitea-Tools issue #420 found the global lock currently held by a live foreign lease:

{
  "issue_number": 108,
  "branch_name": "feat/issue-108-root-menu",
  "remote": "prgs",
  "repo": "mcp-control-plane",
  "worktree_path": ".../mcp-control-plane/branches/issue-108-root-menu",
  "work_lease": {
    "claimant": {
      "username": "sysadmin",
      "profile": "prgs-reviewer"
    },
    "created_at": "2026-07-07T17:42:29Z",
    "expires_at": "2026-07-07T21:42:29Z",
    "last_heartbeat_at": "2026-07-07T17:42:29Z"
  }
}

This was a live, unexpired foreign lease for:

The same global slot had recently held other issues such as #427 and #428, proving it is hot and shared across concurrent sessions.

Impact

  1. One session can silently evict another session's active issue lock.
  2. A server restart can wipe a valid lock and leave a pushed branch stranded.
  3. gitea_lock_issue may refuse to reacquire the lock because the branch already exists.
  4. gitea_create_pr may require a lock that can no longer be safely restored.
  5. Manual lock seeding is unsafe because it can clobber an unrelated live lease.
  6. This creates nondeterministic PR creation failures and cross-session interference.

Observed #420 case

Required fix

Replace the single global lock file with repo/session-safe lock ownership.

Suggested direction (author's choice on exact storage):

  • Key locks by remote + org + repo + issue_number (and/or branch_name).
  • Store multiple concurrent leases without a single overwrite-prone slot.
  • Use atomic create/rename/flock writes; never read-modify-write a shared JSON blob as the sole guard.
  • Persist locks so MCP server restart does not strand pushed branches (or provide an explicit durable recovery path that does not require manual seeding).

Acceptance criteria

  1. Locks must be keyed by at least: remote, repo, issue_number, branch_name, claimant username/profile, and session or process identity where available.
  2. Concurrent sessions for different repos/issues must not share one overwrite-prone slot.
  3. Writes must be atomic and must preserve unrelated foreign leases.
  4. A session must never silently clobber a live foreign lease.
  5. Lock acquisition must fail closed when a true competing live lock exists.
  6. Stale lock recovery must require explicit stale proof: expired lease, missing heartbeat, dead process/session proof where available.
  7. Existing branch recovery must support safe own-branch adoption when:
    • branch name exactly matches requested branch
    • issue number matches
    • no PR already exists
    • no competing live lock exists
    • claimant/worktree proof is valid
  8. gitea_create_pr must not require unsafe global lock seeding after a server restart.
  9. Add tests for:
    • concurrent locks for different repos do not overwrite each other
    • concurrent locks for different issues in same repo do not overwrite each other
    • live foreign lease blocks clobbering
    • stale lease can be recovered with proof
    • own-branch adoption after restart succeeds safely
    • existing PR blocks duplicate PR creation
    • true competing branch remains blocked
  10. Update docs/handoffs to prohibit manual seeding of the global lock file as a normal recovery path.

Non-goals

  • Do not weaken duplicate-work protection (#400).
  • Do not allow arbitrary branch adoption.
  • Do not make branch deletion the normal recovery path.
  • Do not bypass capability checks or workspace guards (#274).

Relationship to existing issues

This is the architectural root defect underlying several open follow-ups:

  • #438 — concurrent-session clobbering / atomicity (overlaps; should be absorbed or blocked by this fix)
  • #440 — non-destructive recovery after lost locks (depends on durable/multi-key storage)
  • #442 — own-branch adoption path (complementary; adoption rules belong in AC #7)
  • #400 — duplicate-work branch guard (must remain fail-closed for real competing work)
  • #274 — branches-only workspace guard (unchanged)
  • #108 — mcp-control-plane menu work held the foreign live lease observed during #420 recovery

Code pointers

  • gitea_mcp_server.py: ISSUE_LOCK_FILE, _load_existing_issue_lock, gitea_lock_issue, gitea_create_pr lock checks
  • issue_lock_worktree.py: worktree validation and lock assessment
  • scripts/worktree-start: reads /tmp/gitea_issue_lock.json fail-closed
  • merged_cleanup_reconcile.py: GITEA_ISSUE_LOCK_FILE env override
## Problem The MCP issue-lock mechanism uses one shared global file: `/private/tmp/gitea_issue_lock.json` (`ISSUE_LOCK_FILE = "/tmp/gitea_issue_lock.json"` in `gitea_mcp_server.py`; same path on macOS via `/private/tmp`.) That file is shared across every session, user, repo, issue, and role. Concurrent sessions overwrite each other's active locks. This caused or exposed the **#420 PR creation deadlock** and creates cross-session interference risk. ## Observed evidence A recovery attempt for Gitea-Tools issue **#420** found the global lock currently held by a live foreign lease: ```json { "issue_number": 108, "branch_name": "feat/issue-108-root-menu", "remote": "prgs", "repo": "mcp-control-plane", "worktree_path": ".../mcp-control-plane/branches/issue-108-root-menu", "work_lease": { "claimant": { "username": "sysadmin", "profile": "prgs-reviewer" }, "created_at": "2026-07-07T17:42:29Z", "expires_at": "2026-07-07T21:42:29Z", "last_heartbeat_at": "2026-07-07T17:42:29Z" } } ``` This was a live, unexpired foreign lease for: - repo: `mcp-control-plane` - issue: #108 - branch: `feat/issue-108-root-menu` - claimant: `sysadmin` / `prgs-reviewer` The same global slot had recently held other issues such as **#427** and **#428**, proving it is hot and shared across concurrent sessions. ## Impact 1. One session can silently evict another session's active issue lock. 2. A server restart can wipe a valid lock and leave a pushed branch stranded. 3. `gitea_lock_issue` may refuse to reacquire the lock because the branch already exists. 4. `gitea_create_pr` may require a lock that can no longer be safely restored. 5. Manual lock seeding is unsafe because it can clobber an unrelated live lease. 6. This creates nondeterministic PR creation failures and cross-session interference. ## Observed #420 case - **Issue:** #420 - **Branch:** `feat/issue-420-server-code-parity` - **Commit:** `934688a71dd19d9b46369e73fb3ac356ee0cc211` - Branch exists and is valid. - No PR exists. - Issue remains open with `status:in-progress`. - `gitea_lock_issue` refuses reacquisition because matching branch already exists. - `gitea_create_pr` requires the wiped lock. - Manual lock restore would clobber another active session's lease. ## Required fix Replace the single global lock file with **repo/session-safe lock ownership**. Suggested direction (author's choice on exact storage): - Key locks by `remote` + `org` + `repo` + `issue_number` (and/or `branch_name`). - Store multiple concurrent leases without a single overwrite-prone slot. - Use atomic create/rename/`flock` writes; never read-modify-write a shared JSON blob as the sole guard. - Persist locks so MCP server restart does not strand pushed branches (or provide an explicit durable recovery path that does not require manual seeding). ## Acceptance criteria 1. Locks must be keyed by at least: `remote`, `repo`, `issue_number`, `branch_name`, claimant username/profile, and session or process identity where available. 2. Concurrent sessions for different repos/issues must not share one overwrite-prone slot. 3. Writes must be atomic and must preserve unrelated foreign leases. 4. A session must never silently clobber a live foreign lease. 5. Lock acquisition must fail closed when a true competing live lock exists. 6. Stale lock recovery must require explicit stale proof: expired lease, missing heartbeat, dead process/session proof where available. 7. Existing branch recovery must support safe own-branch adoption when: - branch name exactly matches requested branch - issue number matches - no PR already exists - no competing live lock exists - claimant/worktree proof is valid 8. `gitea_create_pr` must not require unsafe global lock seeding after a server restart. 9. Add tests for: - concurrent locks for different repos do not overwrite each other - concurrent locks for different issues in same repo do not overwrite each other - live foreign lease blocks clobbering - stale lease can be recovered with proof - own-branch adoption after restart succeeds safely - existing PR blocks duplicate PR creation - true competing branch remains blocked 10. Update docs/handoffs to prohibit manual seeding of the global lock file as a normal recovery path. ## Non-goals - Do not weaken duplicate-work protection (#400). - Do not allow arbitrary branch adoption. - Do not make branch deletion the normal recovery path. - Do not bypass capability checks or workspace guards (#274). ## Relationship to existing issues This is the **architectural root defect** underlying several open follow-ups: - **#438** — concurrent-session clobbering / atomicity (overlaps; should be absorbed or blocked by this fix) - **#440** — non-destructive recovery after lost locks (depends on durable/multi-key storage) - **#442** — own-branch adoption path (complementary; adoption rules belong in AC #7) - **#400** — duplicate-work branch guard (must remain fail-closed for real competing work) - **#274** — branches-only workspace guard (unchanged) - **#108** — mcp-control-plane menu work held the foreign live lease observed during #420 recovery ## Code pointers - `gitea_mcp_server.py`: `ISSUE_LOCK_FILE`, `_load_existing_issue_lock`, `gitea_lock_issue`, `gitea_create_pr` lock checks - `issue_lock_worktree.py`: worktree validation and lock assessment - `scripts/worktree-start`: reads `/tmp/gitea_issue_lock.json` fail-closed - `merged_cleanup_reconcile.py`: `GITEA_ISSUE_LOCK_FILE` env override
jcwalker3 added the status:in-progress label 2026-07-07 15:19:55 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #443 - branch: pending - phase: claimed - profile: prgs-author - pr: none - blocker: none - next_action: create worktree and begin implementation
sysadmin removed the status:in-progress label 2026-07-07 16:57:56 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#443