{"record":{"id":"d7d28ff7bfe1f09d","repo":"gastownhall/beads","slug":"errversionmismatch-d7d28f","errorCode":"ErrVersionMismatch","errorMessage":"%w: expected %d, got %d","messagePattern":"%w: expected (.+?), got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/uow/deleter.go","lineNumber":132,"sourceCode":"\t\treturn publicops.DeleteResult{}, &publicops.NotFoundError{IDs: missing}\n\t}\n\n\t// The version precondition, between the existence probe and the dependents\n\t// guard exactly as issueops.Deleter.Delete orders them.\n\t//\n\t// This leg compares the row the probe already loaded rather than issuing\n\t// its own guard read, which is what updatePreconditionsHold does for the\n\t// same token on the update path. The row was read inside this unit of work,\n\t// so the comparison and the deletion still see one snapshot; the sentinel\n\t// and the message are the shared ones, because a caller matching\n\t// ErrVersionMismatch must not have to know which backend answered.\n\t//\n\t// req.IDs[0] is the only distinct id: ValidateDeleteRequest refused a\n\t// multi-id request carrying a version and NormalizeDeleteIDs collapsed the\n\t// duplicates before either ran.\n\tif req.ExpectedVersion != nil {\n\t\tif current := present[req.IDs[0]].RowVersion; current != *req.ExpectedVersion {\n\t\t\treturn publicops.DeleteResult{}, fmt.Errorf(\"%w: expected %d, got %d\",\n\t\t\t\tpublicops.ErrVersionMismatch, *req.ExpectedVersion, current)\n\t\t}\n\t}\n\n\t// The guard runs only when the request did not already say what to do\n\t// about dependents. Under Cascade there is nothing outside the set by\n\t// construction.\n\tif !req.Cascade {\n\t\tidSet := make(map[string]bool, len(req.IDs))\n\t\tfor _, id := range req.IDs {\n\t\t\tidSet[id] = true\n\t\t}\n\t\texternal, err := externalDependentsBySourceInUOW(ctx, uw, req.IDs, idSet)\n\t\tif err != nil {\n\t\t\treturn publicops.DeleteResult{}, err\n\t\t}\n\t\tif !req.Force {\n\t\t\t// Request order, so the id a caller is told about is stable","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/uow/deleter.go#L114-L150","documentation":"An optimistic-concurrency failure on delete: the request carried an ExpectedVersion and the row's current RowVersion does not match, so the delete refuses to proceed rather than silently deleting a row modified by someone else. Applies only to single-id requests (multi-id versioned deletes are refused in validation).","triggerScenarios":"deleteInUOW compares present[req.IDs[0]].RowVersion against *req.ExpectedVersion and they differ — i.e. the issue/wisp was updated (or its version bumped) between the caller reading it and issuing the delete.","commonSituations":"Two agents/users editing the same issue concurrently; a stale UI or cached copy issuing a versioned delete after another writer updated the issue; automated scripts re-running against already-modified issues.","solutions":["Re-read the issue to get its current RowVersion, then retry the delete with the fresh version","If the delete does not need concurrency protection, omit ExpectedVersion","Use a reconcile flow: detect mismatch, merge/reconcile changes, then delete"],"exampleFix":"// before\nreq := publicops.DeleteRequest{IDs: []string{\"bd-1\"}, ExpectedVersion: &staleVersion}\n// after\nissue, _ := store.GetIssue(ctx, \"bd-1\") // refresh\nreq := publicops.DeleteRequest{IDs: []string{\"bd-1\"}, ExpectedVersion: &issue.RowVersion}","handlingStrategy":"retry","validationCode":"cur, err := store.GetIssue(ctx, id)\nif err != nil { return err }\nif req.ExpectedVersion != nil && cur.RowVersion != *req.ExpectedVersion {\n    return fmt.Errorf(\"stale read: expected %d, current %d\", *req.ExpectedVersion, cur.RowVersion)\n}","typeGuard":"func isVersionMismatch(err error) bool {\n    return errors.Is(err, publicops.ErrVersionMismatch)\n}","tryCatchPattern":"res, err := deleteInUOW(ctx, req)\nif errors.Is(err, publicops.ErrVersionMismatch) {\n    fresh, _ := store.GetIssue(ctx, req.IDs[0])\n    req.ExpectedVersion = &fresh.RowVersion // re-read and retry once\n    res, err = deleteInUOW(ctx, req)\n}","preventionTips":["Always re-read the row immediately before a versioned delete","Omit ExpectedVersion when concurrent modification is acceptable","Serialize writes to the same issue through one actor/queue","Detect ErrVersionMismatch and refresh rather than loop blindly"],"tags":["concurrency","optimistic-locking","delete","version-conflict"],"backgroundTag":"version-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}