{"record":{"id":"57595613dd38541d","repo":"gastownhall/beads","slug":"w-expected-d-got-d","errorCode":null,"errorMessage":"%w: expected %d, got %d","messagePattern":"%w: expected (.+?), got (.+?)","errorType":"error_code","errorClass":"storage.ErrVersionMismatch","httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/version.go","lineNumber":45,"sourceCode":"//nolint:gosec // G201: table name comes from WispTableRouting (hardcoded constants)\nfunc CheckVersionInTx(ctx context.Context, tx DBTX, id string, expected int64) error {\n\tisWisp := IsActiveWispInTx(ctx, tx, id)\n\tissueTable, _, _, _ := WispTableRouting(isWisp)\n\n\t// row_lock is NOT NULL DEFAULT 0, but scan defensively so a NULL maps to 0\n\t// rather than erroring (mirrors scan.go's RowVersion handling).\n\tvar current sql.NullInt64\n\terr := tx.QueryRowContext(ctx,\n\t\tfmt.Sprintf(\"SELECT row_lock FROM %s WHERE id = ?\", issueTable), id,\n\t).Scan(&current)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read row version for %s: %w\", id, err)\n\t}\n\tif current.Int64 != expected {\n\t\treturn fmt.Errorf(\"%w: expected %d, got %d\", storage.ErrVersionMismatch, expected, current.Int64)\n\t}\n\treturn nil\n}\n","sourceCodeStart":27,"sourceCodeEnd":49,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/version.go#L27-L49","documentation":"The core version-conflict branch: the row's current row_lock value differs from the caller's expected version, so the write would clobber another writer's changes. storage.ErrVersionMismatch is returned with expected and actual versions, letting callers implement compare-and-swap retries.","triggerScenarios":"Two sessions read the same issue version and both attempt ExecuteUpdate/Close/Delete/Reopen; the loser's expected version no longer matches the row's row_lock after the winner commits.","commonSituations":"Multiple agents editing the same issue concurrently; retry loops reusing an old version value after a successful first write; daemon and CLI operating on the same issue simultaneously; stale exports replayed against an updated DB.","solutions":["Re-read the issue to get the fresh row_lock, reapply your changes on top, and retry the mutation with the new version","Check errors.Is(err, storage.ErrVersionMismatch) and implement bounded retry (e.g. 3 attempts) rather than failing outright","Merge changes semantically on retry — some fields may have already been changed by the winner","Serialize conflicting work via assignment/claim CAS instead of raw version retries when contention is high"],"exampleFix":"// before\n_, err := store.Update(ctx, id, map[string]interface{}{\"row_lock\": ver, ...})\nif err != nil { return err } // gives up on conflict\n// after\nfor i := 0; i < 3; i++ {\n    iss, _ := store.GetIssue(ctx, id)\n    _, err := store.Update(ctx, id, map[string]interface{}{\"row_lock\": iss.RowLock, ...})\n    if err == nil { return nil }\n    if !errors.Is(err, storage.ErrVersionMismatch) { return err }\n}","handlingStrategy":"retry","validationCode":"iss, _ := store.GetIssue(ctx, id)\nif iss.RowLock != expected { return fmt.Errorf(\"stale version: have %d, want %d\", iss.RowLock, expected) }","typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    err := versionedMutate(ctx, tx, id, expected)\n    if err == nil { break }\n    if errors.Is(err, storage.ErrVersionMismatch) {\n        iss, _ := store.GetIssue(ctx, id)\n        expected = iss.RowLock // rebase and retry\n        continue\n    }\n    return err\n}","preventionTips":["Always re-fetch row_lock before retrying a conflicted mutation","Bound retry attempts and merge changes semantically on rebase","Avoid long-lived snapshots of versions in agents/daemons","Prefer claim-based coordination to reduce write contention"],"tags":["go","optimistic-lock","concurrency","version-conflict"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}