plandex-ai/plandex · error

replacement not found: %s

Error message

replacement not found: %s

What it means

RejectReplacement parsed the result and scanned result.Replacements for one whose Id matches replacementId; none matched. Note the function marks replacement.RejectedAt in memory but (as written in this region) returns nil without persisting — the error is purely about lookup failure.

Source

Thrown at app/server/db/result_helpers.go:1023

	}

	if result.RejectedAt != nil {
		return nil
	}

	now := time.Now()

	foundReplacement := false
	for _, replacement := range result.Replacements {
		if replacement.Id == replacementId {
			replacement.RejectedAt = &now
			foundReplacement = true
			break
		}
	}

	if !foundReplacement {
		return fmt.Errorf("replacement not found: %s", replacementId)
	}

	return nil
}

func GetPlanApplies(orgId, planId string) ([]*PlanApply, error) {
	appliesDir := getPlanAppliesDir(orgId, planId)
	files, err := os.ReadDir(appliesDir)

	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}

		return nil, fmt.Errorf("error reading applies dir: %v", err)
	}

	planApplies := []*PlanApply{}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Confirm replacementId is a member of that result's Replacements (list the result's replacements first).
  2. Check you did not swap resultId and replacementId arguments.
  3. Re-fetch the result to get fresh replacement ids after any plan updates.

Example fix

// before
db.RejectReplacement(orgId, planId, resultId, replacementId)
// after
result := fetchResult(resultId) // verify replacement belongs to this result
for _, rep := range result.Replacements {
    if rep.Id == replacementId {
        return db.RejectReplacement(orgId, planId, resultId, replacementId)
    }
}
return fmt.Errorf("replacement %s is not on result %s", replacementId, resultId)
Defensive patterns

Strategy: validation

Validate before calling

result := getResult(resultId) // fetch and parse the result first
valid := false
for _, rep := range result.Replacements {
    if rep.Id == replacementId {
        valid = true
        break
    }
}
if !valid {
    return fmt.Errorf("replacement %s is not on result %s", replacementId, resultId)
}
return RejectReplacement(orgId, planId, resultId, replacementId)

Try / catch

if err := RejectReplacement(orgId, planId, resultId, replacementId); err != nil {
    if strings.Contains(err.Error(), "replacement not found") {
        return fmt.Errorf("stale replacementId %s; refresh result and retry", replacementId)
    }
    return err
}

Prevention

When it happens

Trigger: Calling RejectReplacement with a replacementId that is not in the result's Replacements slice — wrong id, id from a different result, or the result has an empty Replacements list.

Common situations: Passing the result id as the replacement id (or vice versa); the replacement was already removed/rewritten; client cached ids from before the plan changed.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/5d46424efd8abf2e. Report an issue: GitHub.