gastownhall/beads · error · issueops.ErrValidation
%w: apply batch item %d guards on a row version, but an earl
Error message
%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
What it means
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.
Source
Thrown at internal/storage/batch_apply.go:296
}
// checkApplyExpectedVersion refuses a version guard on a row an earlier item of
// this request already mutated.
//
// IT IS A REQUEST-SHAPE RULE, not a race. The token is server-minted and
// rewritten by the write, so mid-request there is no value a caller COULD send:
// the pre-request token is stale by construction, and a row this request just
// created never had one the caller could read. Refusing statically says so;
// letting it through would answer every such request with ErrVersionMismatch
// and leave the caller looking for a concurrent writer that does not exist.
//
// ExpectedStatus and ExpectedAssignee carry no such rule, and the difference is
// that a caller CAN know what its own earlier item set them to.
func checkApplyExpectedVersion(expected *int64, target issueops.Ref, index int, touched map[string]bool) error {
if expected == nil || !touched[applyTouchKeyRef(target)] {
return nil
}
return fmt.Errorf("%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",
issueops.ErrValidation, index, applyRefLabel(target))
}
// validateApplyTargetRef checks a ref used to ADDRESS a row: exactly one member
// set, and a key that reaches BACKWARD.
func validateApplyTargetRef(ref issueops.Ref, index int, member string, keyIndex map[string]int) error {
if err := validateApplyRef(ref, index, member); err != nil {
return err
}
if ref.Key == "" {
return nil
}
declaredAt, ok := keyIndex[ref.Key]
if !ok {
return &issueops.RefError{Index: index, Member: member, Key: ref.Key}
}
if declaredAt >= index {View on GitHub (pinned to 71377f2769)
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
Example fix
// before
{Kind: issueops.ItemClose, Close: &issueops.CloseItem{Ref: ref, ExpectedVersion: &v}} // after an update to same row in this batch
// after
{Kind: issueops.ItemClose, Close: &issueops.CloseItem{Ref: ref}} // ordering within the batch provides the guard Defensive patterns
Strategy: validation
Validate before calling
touched := map[string]bool{}
for i, it := range items {
ref := itemTargetRef(it)
if ref == nil { continue }
if expectedVersionOf(it) != nil && touched[refKey(ref)] {
return fmt.Errorf("item %d re-guards a row written earlier in this batch", i)
}
touched[refKey(ref)] = true
} Type guard
func unsatisfiableVersionGuard(expected *int64, wroteEarlier bool) bool {
return expected != nil && wroteEarlier
} Try / catch
if err := store.PlanApplyBatch(plan); err != nil {
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "cannot be known mid-request") {
// clear ExpectedVersion on later items touching the same row, or split batches
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- %w: apply batch item %d is kind %q but carries another kind'
- %w: apply batch item %d requires an issue
- %w: apply batch item %d must not carry comments or dependenc
- %w: apply batch item %d reuses key %q, already declared by i
- %w: apply batch item %d has a metadata_ref with an empty key
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/360e7b755cfa455b.
Report an issue: GitHub.