Per-Issue Concurrency Locks
Prevents two sequant sessions from working on the same GitHub issue at the same time. Lock files live at .sequant/locks/<issue>.lock; the lock is acquired automatically when you run sequant run (or /fullsolve, chain mode) and released when the run ends.
You don’t normally see this feature — it just blocks the second session with a clear error. The commands below are for when something goes wrong (a crashed prior session left a stale lock, or you genuinely want to take over).
Prerequisites
Section titled “Prerequisites”- sequant installed —
sequant --version - Local filesystem repo — locks use
open(O_CREAT | O_EXCL)on.sequant/locks/; not safe on NFS / networked filesystems (false positives possible).
What Triggers a Lock
Section titled “What Triggers a Lock”| Command | Acquires lock | Notes |
|---|---|---|
sequant run <issue> | yes | One lock per issue in the run |
sequant fullsolve <issue> | yes | Inherited via the parent process |
| Chain mode | yes | Per-issue, as each issue starts |
sequant assess, sequant merge, sequant status | no | Read-only; checks lock and warns, then proceeds |
MCP / orchestrator runs (SEQUANT_ORCHESTRATOR=1) | no | Lock filesystem is bypassed entirely |
Sub-skills (/spec, /exec, /qa, /loop, /testgen) | no | Inherit the parent’s lock |
What You See When Blocked
Section titled “What You See When Blocked”When you try to run an issue another session already holds:
Issue #604 is being worked on by PID 12345 since 2026-05-10T14:32:00Z(npx sequant fullsolve 604). Use --force to take over, or wait forthe other session.In a batch run, locked issues are skipped and the rest continue. The summary table shows the lock holder:
SUMMARY · 3 issues · 8m 12s · 2 passed · 1 locked
#608 ✔ passed spec → exec → qa · PR #623 #614 ✔ passed exec → qa · PR #615 #604 ⚠ locked by PID 12345 (npx sequant fullsolve 604)The batch only fails (non-zero exit) when every issue is locked.
Stale Lock Recovery
Section titled “Stale Lock Recovery”The lock detects abandoned holders automatically before blocking:
- Same host, dead PID (
process.kill(pid, 0)fails) → cleared immediately. - Same host, alive PID → genuine collision; you’re blocked.
- Cross-host or unknown PID → blocked until the lock is older than 2 hours, then cleared.
So a Ctrl+C or SIGTERM on a sequant run normally releases the lock via ShutdownManager. A hard SIGKILL leaves the file behind, but the next run on the same host auto-clears it.
Taking Over a Lock
Section titled “Taking Over a Lock”When you know the other session is dead and you don’t want to wait:
Pass --force to sequant run
Section titled “Pass --force to sequant run”npx sequant run 604 --forceThis writes a new lock claiming the issue. It does not signal the prior process — use only when you know the other session is dead. --force here serves double duty (state-guard bypass + lock takeover).
--force --signal-other — SIGTERM the prior holder first
Section titled “--force --signal-other — SIGTERM the prior holder first”npx sequant run 604 --force --signal-otherWhen the prior holder is on the same host AND alive AND not this process or its parent, SIGTERMs it before taking the lock. Anything else falls back to a plain force takeover.
The CLI prints one of the following lines so you can see exactly why a SIGTERM was or wasn’t sent (#637):
| Outcome | Log line |
|---|---|
| Signal delivered | Signaled PID <pid> (SIGTERM) for #<issue> |
| Holder on a different host | Could not signal PID <pid> for #<issue> (cross-host holder) |
| Holder PID equals this process or its parent (defense-in-depth guard) | Refused to signal PID <pid> for #<issue> (matches this process or its parent) |
| Holder PID is no longer alive | Could not signal PID <pid> for #<issue> (already exited) |
process.kill syscall failed | Could not signal PID <pid> for #<issue> (kill syscall failed) |
The “matches this process or its parent” line should be vanishingly rare in practice — it fires only if a lock file is malformed or a PID was recycled. Treat it as a heads-up that the lock file is suspect, then proceed with the plain force takeover.
Manual clear via sequant locks
Section titled “Manual clear via sequant locks”For inspection or surgical recovery without launching a run:
# See every active locknpx sequant locks list
# Safety-checked clear (refuses if same-host alive)npx sequant locks clear 604
# Force clear (bypass safety check)npx sequant locks clear 604 --forceWhat to Expect
Section titled “What to Expect”- Auto-acquired and auto-released. You don’t call the lock commands during normal use.
Ctrl+Creleases.ShutdownManagercleans the lock on SIGINT/SIGTERM. Crash recovery handles SIGKILL via PID check on the next run.- MCP runs are unaffected. With
SEQUANT_ORCHESTRATOR=1set, no.sequant/locks/files are created and no checks run. Orchestrator callers coordinate themselves. - NFS is unreliable. If your repo lives on a network filesystem,
O_CREAT | O_EXCLmay report false collisions. Move the repo to local disk if you hit phantom locks.
sequant locks Reference
Section titled “sequant locks Reference”| Subcommand | Use |
|---|---|
locks list | Show all active locks with PID, host, age, and command. --json available. |
locks clear <issue> | Manually clear a lock. Refuses to clear a same-host alive lock unless --force. |
locks acquire <issue> | Claim a lock (used internally by skill shells). --force and --signal-other available. |
locks release <issue> | Release a lock previously acquired by the current process. |
locks check <issue> | Read-only: print holder if held, exit 1 when held. Used by /assess. |
locks check-batch <issues...> | Read-only batch probe; emits canonical ⚠ locked by … lines for held issues. |
Every subcommand accepts --json for scripting.
| Run-level flag | Effect |
|---|---|
sequant run … --force | Take over the lock; also bypasses the completed-issue state guard. |
sequant run … --force --signal-other | SIGTERM the prior same-host holder before taking over. |
Environment
Section titled “Environment”| Variable | Effect |
|---|---|
SEQUANT_ORCHESTRATOR=1 | All lock operations become no-ops. MCP-driven runs bypass the lock filesystem. |
SEQUANT_LOCKS_DIR=<path> | Override .sequant/locks/ location (used by tests; rarely useful elsewhere). |
Troubleshooting
Section titled “Troubleshooting””Issue #N is being worked on by PID …” but I closed that terminal
Section titled “”Issue #N is being worked on by PID …” but I closed that terminal”Cause: The prior session exited without releasing — usually a SIGKILL or crash.
Fix: Re-run; the same-host PID check auto-clears the stale lock on the next attempt. If it persists, run npx sequant locks clear <issue>.
Lock won’t clear even though the PID is gone
Section titled “Lock won’t clear even though the PID is gone”Cause: The lock was created on a different hostname, so the same-host PID check is skipped. The 2-hour age fallback applies.
Fix: npx sequant locks clear <issue> --force, or wait for the 2h age to expire.
Two runs on different machines both think they hold the lock
Section titled “Two runs on different machines both think they hold the lock”Cause: Repo is on a network filesystem; O_CREAT | O_EXCL is not atomic on NFS.
Fix: Move the repo to a local disk. Networked filesystems are explicitly unsupported.
I want /assess to refuse to run when an issue is locked
Section titled “I want /assess to refuse to run when an issue is locked”Cause: /assess is read-only by design and only warns. This is intentional — you can still inspect a locked issue.
Fix: Wrap the call: npx sequant locks check <issue> && npx sequant assess <issue>.
Documents issue #625 (per-issue concurrency lock).