can1357/oh-my-pi · error · Error

cannot complete a dropped goal

Error message

cannot complete a dropped goal

What it means

completeGoalFromTool refuses to complete a goal whose status is 'dropped'. Dropping is itself a terminal state meaning the goal was abandoned; marking an abandoned goal complete would falsify the outcome record.

Source

Thrown at packages/coding-agent/src/goals/runtime.ts:484

				state: { ...state, enabled: false, goal: dropped },
			});
			await this.#commitState(undefined, { persist: "none", emit: false });
			return dropped;
		});
	}

	async completeGoalFromTool(): Promise<Goal> {
		return await this.#withAccounting(async () => {
			await this.#flushUsageLocked("suppressed");
			const state = this.#getStateClone();
			if (!state?.goal) {
				throw new Error("cannot complete goal because no goal is active");
			}
			if (state.goal.status === "complete") {
				throw new Error("goal is already complete");
			}
			if (state.goal.status === "dropped") {
				throw new Error("cannot complete a dropped goal");
			}
			state.enabled = false;
			state.goal.status = "complete";
			state.goal.updatedAt = this.#now();
			state.mode = "exiting";
			state.reason = "completed";
			this.#clearActiveAccounting();
			this.#budgetReportedFor = undefined;
			await this.#commitState(state, { persist: "goal" });
			return state.goal;
		});
	}

	buildActivePrompt(): string | undefined {
		const state = this.#host.getState();
		return state?.enabled && state.goal && state.goal.status === "active"
			? renderGoalPrompt("active", state.goal)
			: undefined;

View on GitHub (pinned to 9690622007)

Solutions

  1. Use the drop op as the terminal action — do not call complete afterwards
  2. Check state.goal.status before completing and skip when 'dropped'
  3. Cancel pending completion tool calls once a drop is issued

Example fix

// before
await dropGoal();
await runtime.completeGoalFromTool(); // throws: dropped
// after
await dropGoal();
const state = host.getState();
if (state?.goal?.status === 'active' || state?.goal?.status === 'budget-limited') {
  await runtime.completeGoalFromTool();
}
Defensive patterns

Strategy: validation

Validate before calling

const state = host.getState();
if (state?.goal?.status === 'dropped') throw new Error('goal was dropped; cannot complete');

Type guard

function isCompletable(state: { goal?: { status: string } } | undefined | null): boolean {
  return !!state?.goal && !['complete', 'dropped'].includes(state.goal.status);
}

Try / catch

try {
  await runtime.completeGoalFromTool();
} catch (err) {
  if (err instanceof Error && err.message.includes('dropped goal')) {
    return { ok: false, reason: 'goal-dropped' }; // cancellation already decided
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling the completion tool after the goal was dropped via the drop op; flows that drop-then-complete; a stale agent turn holding a pre-drop reference that still calls complete.

Common situations: User drops a goal mid-run and the agent then tries to finish it; scripts replaying queued ops without re-reading state; confusion between drop (abandon) and complete (achieve).

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 can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8fa5d4aec44f2e81. Report an issue: GitHub.