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

  1. Refresh the issue list and re-derive the anchors from current positions, then retry the move once.
  2. Send anchors in strict list order: before = the issue above the drop target, after = the issue below.
  3. Apply server-pushed reorder events to local state before issuing the next move.
  4. 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

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


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/6cb0f108c9530af0. Report an issue: GitHub.