/goal: chat "stop" message causes silent token burn until usage limit (autonomous tick ignores LLM-level self-restraint)

Resolved 💬 5 comments Opened May 26, 2026 by jackzhouusa Closed May 26, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

When a goal is in Active state and the user sends a chat message instructing the agent to stop (e.g. "STOP", "pause your work", "do not continue"), the LLM enters a reactive self-restraint mode and emits a "Paused. No action taken..." reply on each autonomous continuation turn. However, the goal's ThreadGoalStatus remains Active, so maybe_continue_goal_if_idle_runtime keeps scheduling continuation turns. Each turn invokes the LLM, and the per-turn output (including the "Paused" message) consumes tokens. Token usage climbs steadily until the 5-hour / weekly usage limit is hit, at which point the goal transitions to UsageLimited.

User-visible: the goal appears paused (model says so on every turn) but tokens continue to burn until quota exhaustion.

Repro

  1. Start a goal: /goal start <multi-step objective> (any objective that requires >30 minutes of continuations).
  2. After at least one continuation turn fires, send a chat message such as: STOP. Do not continue this goal until I explicitly resume it.
  3. Observe the model's reply: Paused. No action taken. The prior STOP still controls; automatic goal continuation is not an explicit restart. (Wording varies per turn — confirming each tick is a fresh LLM call, not a CLI hardcoded line.)
  4. Wait. Subsequent autonomous continuation turns produce the same "Paused" reply repeatedly.
  5. Eventually: Goal hit usage limits (/goal resume). Tokens used + elapsed time reflect the goal kept ticking at full frequency the entire interval.

Expected

Any of:

  • (a) LLM self-restraint should pair with a runtime back-off — after N consecutive turns producing no tool calls and no objective progress, exponentially extend the continuation poll interval (e.g. 1 min → 10 min → 1 hour) until the next user message.
  • (b) The model should be encouraged via system prompt to call update_goal(status="blocked") when it self-restrains in response to a stop instruction, so the runtime stops scheduling continuations.
  • (c) Add a request_pause tool action so the LLM can cooperatively halt the runtime when it chooses to self-restrain, without having to misuse complete / blocked.

Actual

The autonomous tick (maybe_continue_goal_if_idle_runtime in codex-rs/core/src/goals.rs:1289) only checks ThreadGoalStatus::Active (lines 1311–1319 of the same file) before scheduling another continuation. The LLM has no way to set status to Paused via tool call — update_goal's status enum only accepts complete and blocked (codex-rs/core/src/tools/handlers/goal_spec.rs:65). The user-facing /goal pause TUI command (codex-rs/tui/src/chatwidget/goal_menu.rs) is the only path to set Paused.

Users who instruct the agent via chat (rather than slash commands) therefore have no way to actually pause a goal. The LLM's reactive self-restraint silently burns the full remaining quota.

Why this matters

A common usage pattern: long-running goal, user wants to redirect midway, types wait or stop in chat. They reasonably expect the goal to halt. Instead it appears halted but silently drains quota — sometimes hours of token budget gone with no observable work happening.

The "Paused. No action taken" message itself, repeated over hundreds of turns, is the smoking gun: the wording varies between turns, confirming each is a fresh LLM call producing identical-meaning output, with full per-turn token cost.

Notes

  • Tested on Codex CLI built from a recent clone of openai/codex main.
  • Plan: Pro Lite. Both 5-hour and weekly limits affected.
  • This is not the /goal pause command path — that one works correctly and does halt the tick.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #23164

Powered by Codex Action

etraut-openai contributor · 3 months ago

If a goal is active, the agent is not able to stop or pause it unless it is at an impasse. You can use /goal pause to pause it or /goal clear to clear it.

jackzhouusa · 3 months ago

Thanks for the quick response, @etraut-openai. I want to clarify because I think the original issue may have been misread.

I'm aware that /goal pause (the slash command) works correctly. I noted exactly that in the Notes section of the original report: the /goal pause command path is not what I'm reporting; that one works correctly and does halt the tick.

The bug I'm reporting is specifically about the chat-message path, not the slash-command path. Concretely:

  1. User types "stop" or "pause" or "wait" as a regular chat message (not a slash command), which is common for users who prefer natural language.
  2. The LLM reads it, self-restraints, and emits a "Paused. No action taken..." reply.
  3. ThreadGoalStatus stays Active because the LLM has no tool to set it to Paused. update_goal's status enum only accepts complete and blocked (codex-rs/core/src/tools/handlers/goal_spec.rs:65).
  4. maybe_continue_goal_if_idle_runtime (codex-rs/core/src/goals.rs:1289) sees Active and keeps scheduling continuations.
  5. Each continuation is a real LLM call producing a fresh "Paused" reply (wording varies between turns, confirming each is a new turn, not a cached short-circuit). Tokens drain until quota exhaustion.

The user-visible outcome: the agent says it's paused on every turn, but quota burns the whole interval. That looks like a design-level gap between LLM-level self-restraint and runtime-level scheduling.

The three fixes I proposed in the original Expected section target this gap:

  • (a) Runtime-side back-off when N consecutive turns produce no tool calls and no objective progress.
  • (b) Encourage the LLM via system prompt to call update_goal(status="blocked") when self-restraining in response to a stop instruction.
  • (c) Add a request_pause tool action the LLM can use cooperatively, without having to misuse complete or blocked.

Could the issue be reopened so the chat-stop path can be evaluated separately from the working /goal pause slash-command path? Happy to provide a recorded session log if that would help.

etraut-openai contributor · 3 months ago

The /goal feature is designed to keep the agent moving forward toward the goal unless or until the user stops it. It will not work if you tell the agent to stop or pause. The goal machinery will compel it to keep going. If you want to stop the goal, you need to tell the goal runtime itself. That's done via a /goal pause or /goal clear. Changing it in the way you're suggesting would provide the agent a way to stop working short of the goal, which would defeat the purpose of the feature.

jackzhouusa · 3 months ago

Thanks for the deeper response. Before I push on any feature proposal, I want to flag what may just be a doc/implementation mismatch that's relevant here.

The comment at codex-rs/core/src/goals.rs:339 describes a behavior that, if implemented, would address most of what I reported without giving the agent any new pause channel:

"continuation turns with no counted autonomous activity suppress the next automatic continuation until user/tool/external activity resets it."

That reads as a runtime-side throttle that fires when a turn produced no counted activity, and resets the moment user/tool/external activity reappears. The goal stays Active; the agent stays compelled to move forward; only the polling cadence backs off during empty stretches.

But looking at the implementation, active_this_turn() at line 240 is:

fn active_this_turn(&self) -> bool {
    self.active_goal_id.is_some()
}

It checks whether a goal is set, not whether any counted autonomous activity occurred. TurnStarted (line 843) marks the goal active whenever status is Active or BudgetLimited, regardless of what the turn produced. So the "no counted autonomous activity" suppression the doc describes never actually fires.

The narrow question I'd like to clarify, before any feature discussion: is the doc comment at goals.rs:339 stale, or is the implementation missing the no-counted-activity continuation suppression path it describes?

If it's stale, the doc should probably be updated to reflect that runtime back-off isn't part of the design, and I'll move on from that angle.

If the implementation is missing it, that alone would close most of the chat-stop quota-drain scenario without changing any of the "keep moving forward" semantics. The agent still can't stop the goal; the runtime just stops polling at full frequency when the agent itself is producing no work.

The prompt tension in goal_spec.rs (instructing blocked after 3 recurrences of an impasse, but also forbidding the tool from being used for pause-like cases) is a separate question I'm happy to set aside for now.