plandex-ai/plandex · error

invalid lock scope: %v

Error message

invalid lock scope: %v

What it means

After fetching existing locks, lockRepoDB evaluates acquisition per lock; the read/write logic only handles LockScopeRead and LockScopeWrite. If scope somehow holds any other value at this point (it should have been caught by earlier validation), the loop returns "invalid lock scope: %v", which propagates to lock acquisition retry logic.

Source

Thrown at app/server/db/locks.go:275

		if scope == LockScopeRead {
			// if we're trying to acquire a read lock, we can do so unless there's a conflicting lock
			// a write lock always conflicts with a read lock (regardless of branch)
			// a read lock conflicts if it's for a different branch (since it would need to checkout a different branch in the middle of an already-running read)
			if lock.Scope == LockScopeWrite {
				canAcquire = false
				break
			} else if lock.Scope == LockScopeRead {
				if lockBranch != branch {
					canAcquire = false
					break
				}
			}
		} else if scope == LockScopeWrite {
			// if we're trying to acquire a write lock, we can only do so if there's no other lock (read or write)
			canAcquire = false
			break
		} else {
			err = fmt.Errorf("invalid lock scope: %v", scope)
			return "", err
		}
	}

	if !canAcquire {
		if locksVerboseLogging {
			log.Println("can't acquire lock.", "numRetry:", numRetry)
		}
		conflictErr := errors.New("lock conflict: cannot acquire read/write lock")
		log.Printf("[Lock][%d] can't acquire lock, retrying: %v | reason: %s | now: %s | locks:\n%s\n", goroutineID, conflictErr, params.Reason, now, spew.Sdump(locks))

		return retryWithExponentialBackoff(params.Ctx, conflictErr, numRetry, func(nextAttempt int) (string, error) {
			return lockRepoDB(params, nextAttempt)
		})
	}

	if locksVerboseLogging {
		log.Println("can acquire lock - inserting new lock")

View on GitHub (pinned to e2d772072e)

Solutions

  1. Always pass db.LockScopeRead or db.LockScopeWrite; never construct custom LockScope values.
  2. Call the exported lock API rather than lockRepoDB directly so upfront validation runs.
  3. Extend the acquisition branch if you genuinely added a new scope constant.
  4. Add a default case/test asserting LockScope only has the two valid values.

Example fix

// before
scope := db.LockScope(req.Scope) // arbitrary user string
// after
var scope db.LockScope
switch req.Scope {
case "read": scope = db.LockScopeRead
case "write": scope = db.LockScopeWrite
default: return fmt.Errorf("unsupported scope %q", req.Scope)
}
Defensive patterns

Strategy: validation

Validate before calling

if params.Scope != db.LockScopeRead && params.Scope != db.LockScopeWrite {
    return fmt.Errorf("scope must be LockScopeRead or LockScopeWrite")
}

Type guard

func isKnownLockScope(s db.LockScope) bool {
    return s == db.LockScopeRead || s == db.LockScopeWrite
}

Try / catch

lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil && strings.Contains(err.Error(), "invalid lock scope") {
    return fmt.Errorf("programming error: scope %q is not supported", params.Scope) // do not retry
}

Prevention

When it happens

Trigger: A LockScope value that passes none of the read/write equality checks in the acquisition loop — practically a zero-value/empty scope or a constant mismatch introduced after the initial validation.

Common situations: Custom LockScope constants added by a fork without extending the acquisition branch; scope mutated between validation and the loop; a default-constructed LockRepoParams whose Scope validation was bypassed by calling lockRepoDB directly.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/8118a332cf0c4fd9. Report an issue: GitHub.