multica-ai/multica · warning
move anchors are stale or out of order
Error message
move anchors are stale or out of order
What it means
issueMovePosition computes a fractional-order position for reordering issues. When both `before` and `after` anchor positions are supplied, they must satisfy before < after strictly; otherwise the anchors are stale (the list changed since the client read it) or inverted, and the move is rejected with 'stale or out of order'. This is optimistic concurrency control for drag-and-drop ordering: the server refuses to place an issue between anchors that do not actually bracket a gap.
Source
Thrown at server/internal/handler/issue_move.go:194
FROM issue
WHERE workspace_id = $1 AND id = $2
`, workspaceID, *id).Scan(&position)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusBadRequest, "move anchor not found in this workspace")
} else {
writeIssueTableQueryFailure(w, r, "failed to resolve move anchor")
}
return nil, false
}
return &position, true
}
func issueMovePosition(current float64, before, after *float64) (float64, error) {
switch {
case before != nil && after != nil:
if !(*before < *after) {
return 0, errors.New("move anchors are stale or out of order")
}
position := *before + (*after-*before)/2
if !(position > *before && position < *after) ||
math.IsInf(position, 0) || math.IsNaN(position) {
return 0, errors.New("move anchors are too close; refresh and retry")
}
return position, nil
case before != nil:
position := *before + 1
if math.IsInf(position, 0) || math.IsNaN(position) {
return 0, errors.New("move position is out of range")
}
return position, nil
case after != nil:
position := *after - 1
if math.IsInf(position, 0) || math.IsNaN(position) {
return 0, errors.New("move position is out of range")
}View on GitHub (pinned to 2c0912b6ec)
Solutions
- Refresh the issue list and re-derive the anchors from current positions, then retry the move once.
- Send anchors in strict list order: before = the issue above the drop target, after = the issue below.
- Apply server-pushed reorder events to local state before issuing the next move.
- If the 409/400 persists, fall back to moving to top/bottom (single-anchor form) which has no ordering precondition.
Example fix
// before
moveIssue(id, {beforeId: belowIssue.id, afterId: aboveIssue.id}); // swapped
// after
moveIssue(id, {beforeId: aboveIssue.id, afterId: belowIssue.id}); // before.position < after.position
// on failure: const list = await refetchIssues(); retry with fresh neighbors once Defensive patterns
Strategy: retry
Validate before calling
function deriveAnchors(sortedList, targetIndex) {
// sortedList must be sorted by current position ascending
const before = targetIndex > 0 ? sortedList[targetIndex - 1] : null;
const after = targetIndex < sortedList.length - 1 ? sortedList[targetIndex + 1] : null;
if (before && after && !(before.position < after.position)) {
throw new Error('local list is stale: anchor positions out of order — refetch before moving');
}
return { beforeId: before?.id, afterId: after?.id };
} Type guard
const anchorsAreOrdered = (before, after) => before == null || after == null || before.position < after.position;
Try / catch
try {
await api.moveIssue(issueId, { beforeId, afterId });
} catch (e) {
if (e.status === 400 && /stale or out of order/.test(e.message)) {
const list = await refetchIssues(); // refresh positions
const anchors = deriveAnchors(sortedByPosition(list), newIndex);
await api.moveIssue(issueId, anchors); // exactly one retry
} else {
throw e;
}
} Prevention
- Keep local order synced from server reorder events before issuing the next drag.
- Always sort the local list by position before deriving anchors — never by array index alone.
- Retry at most once after a refresh; a second stale failure means a real conflict to surface to the user.
When it happens
Trigger: POST/PATCH move with before_id and after_id whose current positions satisfy pos(before) >= pos(after) — e.g. another user reordered between the client's read and its move, the client sends anchors in the wrong order (after above before), or the client sends the same issue as both anchors.
Common situations: Two users dragging issues in the same view concurrently; stale client state after a reconnect (positions from an old list snapshot); frontend computing anchor ids from an unsorted array; rapid successive drags where the second request's anchors reflect the pre-first-drag order.
Related errors
- move anchors are too close; refresh and retry
- move position is out of range
- remove stale %s copy %s: %w
- reconcile stale %s: %w
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/6cb0f108c9530af0.
Report an issue: GitHub.