{"record":{"id":"e27fb7711665b89c","repo":"can1357/oh-my-pi","slug":"goal-is-already-complete-e27fb7","errorCode":null,"errorMessage":"goal is already complete","messagePattern":"goal is already complete","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/coding-agent/src/goals/runtime.ts","lineNumber":481,"sourceCode":"\t\t\tawait this.#host.emit({\n\t\t\t\ttype: \"goal_updated\",\n\t\t\t\tgoal: dropped,\n\t\t\t\tstate: { ...state, enabled: false, goal: dropped },\n\t\t\t});\n\t\t\tawait this.#commitState(undefined, { persist: \"none\", emit: false });\n\t\t\treturn dropped;\n\t\t});\n\t}\n\n\tasync completeGoalFromTool(): Promise<Goal> {\n\t\treturn await this.#withAccounting(async () => {\n\t\t\tawait this.#flushUsageLocked(\"suppressed\");\n\t\t\tconst state = this.#getStateClone();\n\t\t\tif (!state?.goal) {\n\t\t\t\tthrow new Error(\"cannot complete goal because no goal is active\");\n\t\t\t}\n\t\t\tif (state.goal.status === \"complete\") {\n\t\t\t\tthrow new Error(\"goal is already complete\");\n\t\t\t}\n\t\t\tif (state.goal.status === \"dropped\") {\n\t\t\t\tthrow new Error(\"cannot complete a dropped goal\");\n\t\t\t}\n\t\t\tstate.enabled = false;\n\t\t\tstate.goal.status = \"complete\";\n\t\t\tstate.goal.updatedAt = this.#now();\n\t\t\tstate.mode = \"exiting\";\n\t\t\tstate.reason = \"completed\";\n\t\t\tthis.#clearActiveAccounting();\n\t\t\tthis.#budgetReportedFor = undefined;\n\t\t\tawait this.#commitState(state, { persist: \"goal\" });\n\t\t\treturn state.goal;\n\t\t});\n\t}\n\n\tbuildActivePrompt(): string | undefined {\n\t\tconst state = this.#host.getState();","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/goals/runtime.ts#L463-L499","documentation":"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.","triggerScenarios":"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.","commonSituations":"Tool retries at the harness level; LLM loops that re-call finish tools; UI double-click on a complete button.","solutions":["Make completion idempotent in the caller: check status === 'complete' first and treat it as success","Suppress duplicate tool invocations in the harness before they reach the runtime","Catch this error and no-op instead of surfacing it as a failure"],"exampleFix":"// before\nawait runtime.completeGoalFromTool(); // throws on repeat\n// after\nconst state = host.getState();\nif (state?.goal?.status !== 'complete') {\n  await runtime.completeGoalFromTool();\n}","handlingStrategy":"try-catch","validationCode":"const state = host.getState();\nif (state?.goal?.status === 'complete') return { ok: true, alreadyComplete: true };","typeGuard":"function isCompletable(state: { goal?: { status: string } } | undefined | null): boolean {\n  return !!state?.goal && !['complete', 'dropped'].includes(state.goal.status);\n}","tryCatchPattern":"try {\n  await runtime.completeGoalFromTool();\n} catch (err) {\n  if (err instanceof Error && err.message === 'goal is already complete') {\n    return; // idempotent success — completion already recorded\n  }\n  throw err;\n}","preventionTips":["Deduplicate tool calls before dispatch (guard against LLM re-emitting finish tools)","Make retry wrappers treat 'already complete' as success","Check goal status before every completion call"],"tags":["state","goals","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}