plandex-ai/plandex · error
delete lock failed after 10 attempts: %w
Error message
delete lock failed after 10 attempts: %w
What it means
retryDeleteLock gives up after maxDeleteRetries (10) attempts to delete a repo_locks row and returns 'delete lock failed after 10 attempts' wrapping the original delete error. The DB lock row may remain, so the plan appears locked until the heartbeat/timeout path cleans it up.
Source
Thrown at app/server/db/locks.go:602
wait = 0
}
log.Printf("[Lock][Retry][%d] Lock/transaction conflict (attempt #%d). Retrying in %s... (cause: %v)", getGoroutineID(), attempt, wait, cause)
select {
case <-ctx.Done():
log.Printf("[Lock][Retry][%d] Context canceled while waiting to retry: %v", getGoroutineID(), ctx.Err())
return "", fmt.Errorf("context canceled while waiting to retry: %w", ctx.Err())
case <-time.After(wait):
// Proceed with the next attempt.
}
return nextCall(attempt + 1)
}
func retryDeleteLock(ctx context.Context, cause error, attempt int, nextCall func(int) error) error {
if attempt >= maxDeleteRetries {
return fmt.Errorf("delete lock failed after 10 attempts: %w", cause)
}
// retry 10 times, no backoff or maybe a tiny 50ms
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(deleteRetryDelay):
}
return nextCall(attempt + 1)
}
func CleanupActiveLocks(ctx context.Context) error {
log.Println("Cleaning up any active repo locks...")
// Start a transaction with repeatable read isolation level
tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}View on GitHub (pinned to e2d772072e)
Solutions
- Read the wrapped cause — if it is 40001/40P01 the retry loop will likely succeed once the competing transaction ends
- Verify DB connectivity; a full outage will exhaust all 10 fast retries quickly
- Manually delete the orphaned lock row (DELETE FROM repo_locks WHERE id = ...) or wait for heartbeat-based cleanup
- Increase maxDeleteRetries or add exponential backoff to retryDeleteLock for outage resilience
- On failure, ensure the caller still clears its in-memory activeLockIds entry only after a successful delete
Example fix
// before
err := deleteRepoLockDB(id, planId, reason, 0)
if err != nil { log.Printf("release failed: %v", err) }
// after
err := deleteRepoLockDB(id, planId, reason, 0)
if err != nil {
if strings.Contains(err.Error(), "delete lock failed after 10 attempts") {
// alert: lock row may be orphaned; needs manual/cron cleanup
notify.NotifyErr(notify.SeverityError, fmt.Errorf("lock %s orphaned: %w", id, err))
}
} Defensive patterns
Strategy: retry
Validate before calling
if err := Conn.PingContext(shutdown.ShutdownCtx); err != nil {
return fmt.Errorf("db unavailable, release will fail: %w", err)
} Type guard
func isDeleteExhausted(err error) bool {
return err != nil && strings.Contains(err.Error(), "delete lock failed after 10 attempts")
} Try / catch
err := deleteRepoLockDB(id, planId, reason, 0)
if isDeleteExhausted(err) {
// lock row may be orphaned — alert and schedule manual/cron cleanup
notify.NotifyErr(notify.SeverityError, fmt.Errorf("lock %s not released: %w", id, err))
} Prevention
- Confirm DB connectivity before shutdown-triggered releases
- Add a janitor job deleting repo_locks rows with expired heartbeats
- Increase maxDeleteRetries or add backoff for outage resilience
- Alert on this error since orphaned locks block future plans
When it happens
Trigger: Ten consecutive DELETE failures against repo_locks: connection errors, statement timeouts, deadlock (40P01) or serialization (40001) aborts on the delete, each retried after deleteRetryDelay without backoff.
Common situations: Postgres outage or connection pool exhaustion lasting longer than 10 * deleteRetryDelay; deadlock between delete and a concurrent insert/select on repo_locks; network partition during release on shutdown.
Related errors
- error starting transaction: %v
- error scanning repo lock: %v
- error iterating over repo locks: %v
- error removing expired locks: %v
- error inserting new lock: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/11d69790cb21a0b8.
Report an issue: GitHub.