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
- Use action 'replace' (or edit) instead of 'create' to swap an existing active goal
- Clear the existing goal first, then create the new one after complete/clear
- Read the projection's currentGoal and only 'create' when it is null or complete
- 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
- Check the projection's currentGoal before issuing 'create'
- Make goal seeding idempotent (skip when an active goal exists)
- Use 'replace' or 'edit' for changing an active goal
- Disable the create control while a goal is active
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
- goal_not_found
- OpenCode target executable is not a regular file
- Live roster ${rosterId} repeats a case
- native_session_goal_unavailable
- ACPX session is not at a safe suspension point
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/1a9d7be7a883d73e.
Report an issue: GitHub.