can1357/oh-my-pi · warning · Error

goal is already complete

Error message

goal is already complete

What it means

completeGoalFromTool throws when the goal's status is already 'complete'. Completion is a terminal transition; calling it again would double-finalize the goal and corrupt usage accounting, so the runtime rejects the second call.

Source

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

			await this.#host.emit({
				type: "goal_updated",
				goal: dropped,
				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();

View on GitHub (pinned to 9690622007)

Solutions

  1. Make completion idempotent in the caller: check status === 'complete' first and treat it as success
  2. Suppress duplicate tool invocations in the harness before they reach the runtime
  3. Catch this error and no-op instead of surfacing it as a failure

Example fix

// before
await runtime.completeGoalFromTool(); // throws on repeat
// after
const state = host.getState();
if (state?.goal?.status !== 'complete') {
  await runtime.completeGoalFromTool();
}
Defensive patterns

Strategy: try-catch

Validate before calling

const state = host.getState();
if (state?.goal?.status === 'complete') return { ok: true, alreadyComplete: true };

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 === 'goal is already complete') {
    return; // idempotent success — completion already recorded
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling completeGoalFromTool twice (duplicate tool invocation); retrying the completion op after a network/persistence error that actually succeeded; agent re-emitting the completion tool in a follow-up turn.

Common situations: Tool retries at the harness level; LLM loops that re-call finish tools; UI double-click on a complete button.

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/e27fb7711665b89c. Report an issue: GitHub.