paperclipai/paperclip · error · RunnerGoalConflictError

replacement_required

replacement_required

Error message

replacement_required

What it means

Only one active goal may exist per session. If `request.action === "create"` and the session already has a goal whose `status !== "complete"`, `act` throws RunnerGoalConflictError with code `replacement_required`. The client must explicitly replace (or clear) the existing goal instead of creating a second one.

Source

Thrown at server/src/services/runner-goals.ts:437

        : request.action === "resume"
          ? "resume"
          : request.action === "clear"
            ? "clear"
            : "set";
      if (capability.availability !== "available" || !capability.actions.includes(requiredCapabilityAction)) {
        throw new RunnerGoalActionError(
          capability.reasonCode ?? "goal_action_unsupported",
          capability.reason ?? `${request.action} is unsupported by this agent session.`,
        );
      }
      if (request.tokenBudget != null && !capability.tokenBudgetControl) {
        throw new RunnerGoalActionError(
          "goal_token_budget_unsupported",
          "This agent session does not support goal token budgets.",
        );
      }
      if (request.action === "create" && currentGoal && currentGoal.status !== "complete") {
        throw new RunnerGoalConflictError("replacement_required", currentProjection);
      }
      if (["edit", "replace", "pause", "resume", "clear"].includes(request.action) && !currentGoal) {
        throw new RunnerGoalActionError("goal_not_found", "There is no current session goal.");
      }

      const nextRevision = session.goalRevision + 1;
      const desiredState = request.action === "pause"
        ? "paused"
        : request.action === "clear"
          ? null
          : "active";
      await tx.update(agentTaskSessions).set({
        goalDesiredState: desiredState,
        goalRevision: nextRevision,
        updatedAt: new Date(),
      }).where(eq(agentTaskSessions.id, session.id));
      const pendingProjection: RunnerGoalProjection = {
        ...currentProjection,

View on GitHub (pinned to 01ad858492)

Solutions

  1. Use action 'replace' (or edit) instead of 'create' to swap an existing active goal
  2. Clear the existing goal first, then create the new one after complete/clear
  3. Read the projection's currentGoal and only 'create' when it is null or complete
  4. On this conflict, refresh the projection and decide based on the actual current goal

Example fix

// before
await act(companyId, issueId, { agentId, action: 'create', goal: newGoal, ... });
// after
const proj = await projection(companyId, issueId, agentId);
const action = proj.currentGoal && proj.currentGoal.status !== 'complete' ? 'replace' : 'create';
await act(companyId, issueId, { agentId, action, goal: newGoal, ... });
Defensive patterns

Strategy: type-guard

Validate before calling

const proj = await runnerGoals.projection(companyId, issueId, agentId);
const action = proj.currentGoal && proj.currentGoal.status !== 'complete' ? 'replace' : 'create';

Type guard

function canCreateGoal(proj) {
  return proj?.currentGoal == null || proj.currentGoal.status === 'complete';
}

Try / catch

try {
  await runnerGoals.act(companyId, issueId, req);
} catch (e) {
  if (e instanceof RunnerGoalConflictError && e.code === 'replacement_required') {
    return await runnerGoals.act(companyId, issueId, { ...req, action: 'replace' });
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending action 'create' when `storedGoal(session)` returns a live goal (status pending/active/paused, not complete); double-clicking create; automated goal seeding that runs more than once; previous goal never completed and code assumes clean slate.

Common situations: Retry after a partially-processed create that actually stored the goal; UI lacking goal state refresh; scripts that assume idempotent creates without consulting the projection's currentGoal.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/1a9d7be7a883d73e. Report an issue: GitHub.