multica-ai/multica · warning

move anchors are too close; refresh and retry

Error message

move anchors are too close; refresh and retry

What it means

issueMovePosition places the moved issue at the midpoint between the two anchors: position = before + (after-before)/2. After repeated inserts into the same gap, floating-point precision runs out and the midpoint can equal an anchor or become Inf/NaN; the check position > before && position < after catches this. The error tells the client the gap is exhausted and it must refresh so the server (or a rebalance) can re-space positions.

Source

Thrown at server/internal/handler/issue_move.go:199

			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")
		}
		return position, nil
	default:
		return current, nil
	}
}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. On this error, refetch the list and retry once — fresh anchors will have a usable gap (and any rebalancing will have re-spaced values).
  2. Prefer moving to the very top/bottom (single-anchor +1/-1 form) instead of repeatedly subdividing one gap.
  3. If it recurs constantly for the same gap, trigger/request a position rebalance for the list or spread inserts across neighboring gaps.
  4. Client-side, avoid issuing a move whose anchors' positions are already adjacent in float terms.

Example fix

// before
async function drag(issueId, beforePos, afterPos) {
  return api.move(issueId, {before: beforePos, after: afterPos});
}

// after
async function drag(issueId, beforeId, afterId) {
  try { return await api.move(issueId, {before: beforeId, after: afterId}); }
  catch (e) {
    if (e.message.includes('too close')) {
      await refetchList(); // get re-spaced positions
      return api.move(issueId, {before: beforeId, after: afterId}); // one retry
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

function gapIsUsable(beforePos, afterPos) {
  const mid = beforePos + (afterPos - beforePos) / 2;
  return mid > beforePos && mid < afterPos && Number.isFinite(mid);
}

// skip the request if the local gap is already exhausted
if (before && after && !gapIsUsable(before.position, after.position)) {
  await refetchIssues(); // get re-spaced positions before attempting the move
}

Type guard

const hasFloatRoom = (a, b) => b > a && (a + (b - a) / 2) < b;

Try / catch

try {
  await api.moveIssue(issueId, { beforeId, afterId });
} catch (e) {
  if (e.status === 400 && /too close/.test(e.message)) {
    await refetchIssues(); // server-side re-spacing makes the gap usable again
    await api.moveIssue(issueId, { beforeId, afterId });
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Drag-and-drop the same narrow gap hundreds of times so consecutive float64 positions differ by less than one ULP; the midpoint computation rounds to before or after exactly, failing the strict inequality; mathematically (after-before)/2 underflows to 0 at extreme magnitudes.

Common situations: Long-lived boards where users always insert at the same spot (top of list) without full reordering; test suites performing thousands of inserts into one gap; position values grown large after many +1/-1 appends (precision at 2^53 scale).

Related errors


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