goal: idle-continuation prompt is re-parsed as a new objective -> InvalidObjectiveError aborts the session on every turn
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
omo.jsonc:"goal": { "enabled": true, "auto_start": true }- Send any message in a session (this sets a goal via
handleGoalMessage). - 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
- In the goal hook, dispatch the continuation with the internal marker, e.g.
parts: [createInternalAgentTextPart(promptText)](orcreateInternalAgentContinuationTextPart), so internal prompts are excluded from goal parsing — consistent with the other internal dispatchers. - Make
setGoalfailure non-fatal at the hook boundary: either truncate invalidateObjective()instead of throwing, or wrap thesetGoalcalls inhandleGoalMessage()andcreateCommandExecuteBeforeHandler()intry/catch+log2(...). - Optionally only treat an inbound prompt as an objective when it is a real
/goalinvocation (or whenisFirstMessage && 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