Harden shared issue/PR lock handling against concurrent LLM session clobbering #438

Closed
opened 2026-07-07 12:26:59 -05:00 by jcwalker3 · 2 comments
Owner

Problem

During work on issue #306 / PR #423, the shared global lock file /tmp/gitea_issue_lock.json was repeatedly clobbered by concurrent author sessions working on #403 and #407. A concurrent session also opened PR #423 on the same already-pushed branch during the race. This indicates the current lock mechanism may not reliably prevent duplicate ownership, branch races, or cross-session mutation collisions.

The lock is a single global path (ISSUE_LOCK_FILE = "/tmp/gitea_issue_lock.json") updated by normal read-modify-write JSON. gitea_mark_issue / gitea_lock_issue write it; gitea_create_pr reads it and fails closed on a worktree mismatch. Under simultaneous sessions the last writer wins, so a session can lose its lock to another issue's session between claiming and PR creation.

Why this matters

The MCP/Gitea workflow depends on live queue ownership and role separation. If concurrent sessions can overwrite or ignore the shared lock, multiple LLMs may:

  • claim the same issue or branch
  • open duplicate or competing PRs
  • mutate work owned by another session
  • produce misleading handoffs
  • bypass queue-ordering or in-progress protections
  • make reviewer/reconciler state harder to trust

Acceptance criteria

  1. Lock acquisition must be atomic and fail closed under concurrent writers.
  2. Lock ownership must include issue number, repo, branch, role/profile, PID or session identifier if available, timestamp, and expiry/heartbeat metadata.
  3. A session must not silently overwrite another live lock.
  4. Stale lock recovery must require explicit stale proof, such as expired heartbeat or dead-process proof.
  5. Queue selection must treat a live lock as status:in-progress or equivalent.
  6. Branch/PR creation must re-check lock ownership immediately before mutation.
  7. The workflow must produce clear proof in final reports:
    • lock acquired
    • lock owner
    • lock freshness
    • no competing live lock
    • lock released or intentionally retained
  8. Add tests that simulate concurrent lock acquisition attempts and prove only one session can own the issue.
  9. Add tests that prove stale locks can be recovered safely without allowing active-lock clobbering.
  10. Update docs/templates so author/reviewer/reconciler sessions report lock proof consistently.

Suggested implementation direction

Use an atomic filesystem operation such as exclusive create (O_EXCL), rename-based compare/swap, or an OS-level file lock (flock/fcntl) instead of read-modify-write JSON updates. Consider keying the lock per issue rather than a single global file so distinct issues never contend. The exact implementation is the author's choice, but the result must be race-safe under simultaneous LLM sessions.

Related observation

Observed while authoring PR #423 for issue #306. Concurrent sessions around #403/#407 repeatedly clobbered /tmp/gitea_issue_lock.json, and another session opened #423 on the already-pushed branch. See also the current lock code in issue_lock_worktree.py (verify_pr_worktree_matches_lock, base-equivalence gate) and gitea_mcp_server.py (ISSUE_LOCK_FILE).

## Problem During work on issue #306 / PR #423, the shared global lock file `/tmp/gitea_issue_lock.json` was repeatedly clobbered by concurrent author sessions working on #403 and #407. A concurrent session also opened PR #423 on the same already-pushed branch during the race. This indicates the current lock mechanism may not reliably prevent duplicate ownership, branch races, or cross-session mutation collisions. The lock is a single global path (`ISSUE_LOCK_FILE = "/tmp/gitea_issue_lock.json"`) updated by normal read-modify-write JSON. `gitea_mark_issue` / `gitea_lock_issue` write it; `gitea_create_pr` reads it and fails closed on a worktree mismatch. Under simultaneous sessions the last writer wins, so a session can lose its lock to another issue's session between claiming and PR creation. ## Why this matters The MCP/Gitea workflow depends on live queue ownership and role separation. If concurrent sessions can overwrite or ignore the shared lock, multiple LLMs may: * claim the same issue or branch * open duplicate or competing PRs * mutate work owned by another session * produce misleading handoffs * bypass queue-ordering or in-progress protections * make reviewer/reconciler state harder to trust ## Acceptance criteria 1. Lock acquisition must be atomic and fail closed under concurrent writers. 2. Lock ownership must include issue number, repo, branch, role/profile, PID or session identifier if available, timestamp, and expiry/heartbeat metadata. 3. A session must not silently overwrite another live lock. 4. Stale lock recovery must require explicit stale proof, such as expired heartbeat or dead-process proof. 5. Queue selection must treat a live lock as `status:in-progress` or equivalent. 6. Branch/PR creation must re-check lock ownership immediately before mutation. 7. The workflow must produce clear proof in final reports: * lock acquired * lock owner * lock freshness * no competing live lock * lock released or intentionally retained 8. Add tests that simulate concurrent lock acquisition attempts and prove only one session can own the issue. 9. Add tests that prove stale locks can be recovered safely without allowing active-lock clobbering. 10. Update docs/templates so author/reviewer/reconciler sessions report lock proof consistently. ## Suggested implementation direction Use an atomic filesystem operation such as exclusive create (`O_EXCL`), rename-based compare/swap, or an OS-level file lock (`flock`/`fcntl`) instead of read-modify-write JSON updates. Consider keying the lock per issue rather than a single global file so distinct issues never contend. The exact implementation is the author's choice, but the result must be race-safe under simultaneous LLM sessions. ## Related observation Observed while authoring PR #423 for issue #306. Concurrent sessions around #403/#407 repeatedly clobbered `/tmp/gitea_issue_lock.json`, and another session opened #423 on the already-pushed branch. See also the current lock code in `issue_lock_worktree.py` (`verify_pr_worktree_matches_lock`, base-equivalence gate) and `gitea_mcp_server.py` (`ISSUE_LOCK_FILE`).
Author
Owner

Related enforcement wall filed as #447: manual /tmp/gitea_issue_lock.json seeding must be blocked/reported while this atomic repo/session-safe lock redesign is being implemented.

Related enforcement wall filed as #447: manual `/tmp/gitea_issue_lock.json` seeding must be blocked/reported while this atomic repo/session-safe lock redesign is being implemented.
jcwalker3 added the status:in-progress label 2026-07-07 15:22:43 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #438 - branch: feat/issue-438-lock-hardening - 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 17:37:48 -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#438