plandex-ai/plandex · error
error scanning repo lock: %v
Error message
error scanning repo lock: %v
What it means
After querying existing repo_lock rows, lockRepoDB scans each row into a repoLock struct with nine columns (Id, OrgId, UserId, PlanId, PlanBuildId, Scope, Branch, LastHeartbeatAt, CreatedAt). A scan failure — NULL in a non-pointer column or a type mismatch — aborts the lock attempt with "error scanning repo lock: %v".
Source
Thrown at app/server/db/locks.go:212
if forUpdate {
log.Printf("[Lock][%d] SELECT FOR UPDATE took %v | reason: %s",
goroutineID, time.Since(selectStart), params.Reason)
} else {
log.Printf("[Lock][%d] SELECT FOR SHARE took %v | reason: %s",
goroutineID, time.Since(selectStart), params.Reason)
}
}
defer repoLockRows.Close()
var expiredLockIds []string
expiredLockIdsSet := make(map[string]bool)
now := time.Now()
for repoLockRows.Next() {
var lock repoLock
if err := repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, &lock.PlanId, &lock.PlanBuildId, &lock.Scope, &lock.Branch, &lock.LastHeartbeatAt, &lock.CreatedAt); err != nil {
return "", fmt.Errorf("error scanning repo lock: %v", err)
}
// ensure heartbeat hasn't timed out
if now.Sub(lock.LastHeartbeatAt) < lockHeartbeatTimeout {
locks = append(locks, &lock)
} else {
expiredLockIds = append(expiredLockIds, lock.Id)
expiredLockIdsSet[lock.Id] = true
}
}
if err := repoLockRows.Err(); err != nil {
log.Printf("[Lock][%d] error iterating over repo locks: %v | reason: %s", goroutineID, err, params.Reason)
return "", fmt.Errorf("error iterating over repo locks: %v", err)
}
log.Printf("[Lock][%d] %d locks found, %d expired | reason: %s", goroutineID, len(locks), len(expiredLockIds), params.Reason)
View on GitHub (pinned to e2d772072e)
Solutions
- Find the offending row via the wrapped SQL error and fix or delete the NULL/malformed lock row — it will also block acquisition until its heartbeat expires.
- Make nullable columns pointer types (or sql.NullString/NullTime) in repoLock and in Scan so NULLs scan cleanly.
- Confirm the SELECT column list matches repoLock field order/types after any schema migration.
- Wait out the 60s heartbeat timeout — expired locks are deleted automatically, which often clears the bad row.
Example fix
// before var lock repoLock repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, ..., &lock.Branch, ...) // after Branch *string // nullable column in repoLock struct ...repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, ..., &lock.Branch, ...) // NULL-safe
Defensive patterns
Strategy: retry
Validate before calling
// detect stale/broken lock rows before locking
rows, err := db.Conn.Queryx("SELECT * FROM repo_locks WHERE plan_id=$1", planId)
for rows.Next() {
var l db.RepoLock
if err := rows.StructScan(&l); err != nil {
log.Printf("corrupt lock row for plan %s, will rely on heartbeat expiry", planId)
}
} Type guard
func lockRowSane(l *repoLock) bool {
return l.Id != "" && l.OrgId != "" && !l.LastHeartbeatAt.IsZero() && !l.CreatedAt.IsZero()
} Try / catch
lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil && strings.Contains(err.Error(), "error scanning repo lock") {
// corrupt/expired row — retry after heartbeat timeout (60s) clears it
time.Sleep(db.HeartbeatTimeout)
lockId, err = db.LockRepo(ctx, cancel, params)
} Prevention
- Declare nullable repo_locks columns as pointer/Null* types in the struct and Scan.
- Keep the SELECT column list in lockRepoDB in lockstep with repo_locks migrations.
- Never hand-edit lock rows; clean them through code that sets all NOT NULL fields.
When it happens
Trigger: A repo_locks row containing NULL for a column scanned into a non-pointer field (e.g. Branch, PlanBuildId, UserId); schema drift where the SELECT column list/type no longer matches repoLock fields.
Common situations: Old rows written by a prior schema version that lacked a column now scanned; manual DB edits or migrations inserting NULLs; mixing up column order after an ALTER TABLE without updating the query.
Related errors
- unsupported data type: %T
- error running migrations: %v
- error starting transaction: %v
- error iterating over repo locks: %v
- error removing expired locks: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/59dee0d36dff6287.
Report an issue: GitHub.