{"record":{"id":"360e7b755cfa455b","repo":"gastownhall/beads","slug":"w-apply-batch-item-d-guards-on-a-row-version-b","errorCode":null,"errorMessage":"%w: apply batch item %d guards on a row version, but an earlier item in this request already wrote %s; the version token is minted by the write and cannot be known mid-request","messagePattern":"%w: apply batch item (.+?) guards on a row version, but an earlier item in this request already wrote (.+?); the version token is minted by the write and cannot be known mid-request","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/batch_apply.go","lineNumber":296,"sourceCode":"}\n\n// checkApplyExpectedVersion refuses a version guard on a row an earlier item of\n// this request already mutated.\n//\n// IT IS A REQUEST-SHAPE RULE, not a race. The token is server-minted and\n// rewritten by the write, so mid-request there is no value a caller COULD send:\n// the pre-request token is stale by construction, and a row this request just\n// created never had one the caller could read. Refusing statically says so;\n// letting it through would answer every such request with ErrVersionMismatch\n// and leave the caller looking for a concurrent writer that does not exist.\n//\n// ExpectedStatus and ExpectedAssignee carry no such rule, and the difference is\n// that a caller CAN know what its own earlier item set them to.\nfunc checkApplyExpectedVersion(expected *int64, target issueops.Ref, index int, touched map[string]bool) error {\n\tif expected == nil || !touched[applyTouchKeyRef(target)] {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"%w: apply batch item %d guards on a row version, but an earlier item in this request already wrote %s; \"+\n\t\t\"the version token is minted by the write and cannot be known mid-request\",\n\t\tissueops.ErrValidation, index, applyRefLabel(target))\n}\n\n// validateApplyTargetRef checks a ref used to ADDRESS a row: exactly one member\n// set, and a key that reaches BACKWARD.\nfunc validateApplyTargetRef(ref issueops.Ref, index int, member string, keyIndex map[string]int) error {\n\tif err := validateApplyRef(ref, index, member); err != nil {\n\t\treturn err\n\t}\n\tif ref.Key == \"\" {\n\t\treturn nil\n\t}\n\tdeclaredAt, ok := keyIndex[ref.Key]\n\tif !ok {\n\t\treturn &issueops.RefError{Index: index, Member: member, Key: ref.Key}\n\t}\n\tif declaredAt >= index {","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/batch_apply.go#L278-L314","documentation":"An ExpectedVersion guard on an Update/Close item asserts the row is at a version the caller observed earlier. If an earlier item in the SAME batch already wrote that row, the new version token doesn't exist yet (it's minted by that write), so the guard is unsatisfiable and rejected. ExpectedStatus/ExpectedAssignee guards are allowed mid-batch because the caller can know what its own items set.","triggerScenarios":"PlanApplyBatch with item 0 updating issue X, then item 3 closing X with ExpectedVersion set to the pre-batch version — the planner sees X in the touched set and rejects.","commonSituations":"Programmatically generating chains like update-then-close on the same issue and copying ExpectedVersion from the original read into every item; retrying a failed batch by rebuilding it with stale version tokens while keeping multiple writes to one row.","solutions":["Drop ExpectedVersion on the later item(s) touching the same row within one batch (rely on ordering), or split them into separate batch requests","Use ExpectedStatus/ExpectedAssignee guards instead, which are permitted mid-request","Read a fresh version and issue a new batch after the first one is applied"],"exampleFix":"// before\n{Kind: issueops.ItemClose, Close: &issueops.CloseItem{Ref: ref, ExpectedVersion: &v}} // after an update to same row in this batch\n// after\n{Kind: issueops.ItemClose, Close: &issueops.CloseItem{Ref: ref}} // ordering within the batch provides the guard","handlingStrategy":"validation","validationCode":"touched := map[string]bool{}\nfor i, it := range items {\n  ref := itemTargetRef(it)\n  if ref == nil { continue }\n  if expectedVersionOf(it) != nil && touched[refKey(ref)] {\n    return fmt.Errorf(\"item %d re-guards a row written earlier in this batch\", i)\n  }\n  touched[refKey(ref)] = true\n}","typeGuard":"func unsatisfiableVersionGuard(expected *int64, wroteEarlier bool) bool {\n  return expected != nil && wroteEarlier\n}","tryCatchPattern":"if err := store.PlanApplyBatch(plan); err != nil {\n  if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), \"cannot be known mid-request\") {\n    // clear ExpectedVersion on later items touching the same row, or split batches\n  }\n  return err\n}","preventionTips":["Within one batch, guard a row with ExpectedVersion only on its first item","Prefer ExpectedStatus/ExpectedAssignee guards for mid-batch checks","Split multi-write-to-one-row work into sequential batches with fresh version reads"],"tags":["validation","batch-apply","optimistic-concurrency","version-guard"],"backgroundTag":"unsatisfiable-version-guard","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}