goal: idle-continuation prompt is re-parsed as a new objective -> InvalidObjectiveError aborts the session on every turn

Author: souta-labCreated Sep 17, 2026Updated Sep 17, 2026
Labelsopencode

Summary

With goal.enabled: true, every assistant turn ends with a hard session error:

✗ opencode session error: InvalidObjectiveError: Objective exceeds maximum length of 2000 characters
    at validateObjective (.../oh-my-openagent/dist/index.js:107161:36)
    at setGoal (.../oh-my-openagent/dist/index.js:107190:42)
    at handleGoalMessage (...)

Root cause: the goal idle-continuation prompt is re-parsed as a new goal objective and rejected by MAX_OBJECTIVE_LENGTH = 2000. Because the goal stays active, the failure repeats after every turn (observed 69 failures in one day for a single session, one error per assistant turn).

Version: [email protected] (installed as an opencode plugin), opencode on Linux.

Root cause

1. handleGoalMessage() treats every prompt as a goal objective

packages/omo-opencode/src/plugin/chat-message/loop-commands.ts:

const promptText = extractPromptText5(output.parts);
const parsed = parseGoalCommand(promptText);
switch (parsed.kind) {
  case "setObjective":
    hooks2.goal.setGoal(input.sessionID, parsed.objective);

parseGoalCommand() maps anything that is not pause / resume / clear to { kind: "setObjective" }, so every user message and every internal prompt whose text is not one of those keywords becomes the new goal. There is no check that the prompt is actually a /goal <objective> invocation, and no isFirstMessage / auto_start gate on this branch.

2. The idle-continuation prompt is dispatched without the internal marker

packages/omo-opencode/src/hooks/goal/index.ts:

const promptText = buildContinuationPrompt2(goal2);
const promptResult = await dispatchInternalPrompt({
  ...
  input: { path: { id: sessionID }, body: {
    parts: [{ type: "text", text: promptText }]   // <-- raw part, no internal marker
  } }
});

Other internal continuation dispatches (e.g. the todo continuation) send createInternalAgentContinuationTextPart(...) / createInternalAgentTextPart(...), which appends <!-- OMO_INTERNAL_INITIATOR --> (and synthetic: true). Without that marker the part counts as a real user text part (isRealUserTextPart), so the chat-message hook does not skip it and root cause 1 turns it into a new objective.

3. validateObjective() throws out of the hook

packages/omo-opencode/src/hooks/goal/validation.ts:

if (trimmed.length > MAX_OBJECTIVE_LENGTH) {
  throw new InvalidObjectiveError(`Objective exceeds maximum length of ${MAX_OBJECTIVE_LENGTH} characters`);
}

The throw escapes the chat.message hook / command.execute.before hook, so opencode aborts the prompt (prompt_async failed). A validation error in a continuation helper should never be able to kill a session.

Reproduction

  1. omo.jsonc: "goal": { "enabled": true, "auto_start": true }
  2. Send any message in a session (this sets a goal via handleGoalMessage).
  3. Let the turn finish → session.idle → the goal hook dispatches the continuation prompt → InvalidObjectiveError → session error, repeated on every subsequent idle.

Measured length of buildContinuationPrompt2() output for a short one-line objective:

internal goal-continuation prompt length: 2045  -> > 2000: true

So the continuation prompt always exceeds the limit; the loop is deterministic.

Suggested fix

  1. In the goal hook, dispatch the continuation with the internal marker, e.g. parts: [createInternalAgentTextPart(promptText)] (or createInternalAgentContinuationTextPart), so internal prompts are excluded from goal parsing — consistent with the other internal dispatchers.
  2. Make setGoal failure non-fatal at the hook boundary: either truncate in validateObjective() instead of throwing, or wrap the setGoal calls in handleGoalMessage() and createCommandExecuteBeforeHandler() in try/catch + log2(...).
  3. Optionally only treat an inbound prompt as an objective when it is a real /goal invocation (or when isFirstMessage && goal.auto_start), instead of every message.

Local verification of the fix

I patched dist/index.js locally with (1) createInternalAgentTextPart(promptText) for the idle-continuation dispatch and (2) truncation instead of the throw in validateObjective(), then re-executed the patched functions:

A1 truncate to 2000       : PASS
A2 empty still throws     : PASS
A3 short passthrough      : PASS
B1 part has marker        : PASS
B2 not a real user part   : PASS
B3 all-internal => skipped: PASS
B4 normal msg still real  : PASS
B5 omo continuation > 2000: PASS

Minor observation

goal.auto_start appears to be dead config: createGoalHook(ctx, { autoStart: pluginConfig.goal?.auto_start ?? false, ... }) receives options.autoStart, but createGoalHook never reads it (packages/omo-opencode/src/hooks/goal/index.ts). If auto-start is meant to be the only path that sets a goal from a user message, wiring autoStart into handleGoalMessage() would also fix this bug.

Source: code-yeongyu/oh-my-openagent