/goal: chat "stop" message causes silent token burn until usage limit (autonomous tick ignores LLM-level self-restraint)
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
- Start a goal:
/goal start <multi-step objective>(any objective that requires >30 minutes of continuations). - After at least one continuation turn fires, send a chat message such as:
STOP. Do not continue this goal until I explicitly resume it. - 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.) - Wait. Subsequent autonomous continuation turns produce the same "Paused" reply repeatedly.
- 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_pausetool action so the LLM can cooperatively halt the runtime when it chooses to self-restrain, without having to misusecomplete/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/codexmain. - Plan: Pro Lite. Both 5-hour and weekly limits affected.
- This is not the
/goal pausecommand path — that one works correctly and does halt the tick.
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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 pauseto pause it or/goal clearto clear it.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 pausecommand 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:
ThreadGoalStatusstaysActivebecause the LLM has no tool to set it toPaused.update_goal's status enum only acceptscompleteandblocked(codex-rs/core/src/tools/handlers/goal_spec.rs:65).maybe_continue_goal_if_idle_runtime(codex-rs/core/src/goals.rs:1289) seesActiveand keeps scheduling continuations.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:
update_goal(status="blocked")when self-restraining in response to a stop instruction.request_pausetool action the LLM can use cooperatively, without having to misusecompleteorblocked.Could the issue be reopened so the chat-stop path can be evaluated separately from the working
/goal pauseslash-command path? Happy to provide a recorded session log if that would help.The
/goalfeature 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 pauseor/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.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:339describes a behavior that, if implemented, would address most of what I reported without giving the agent any new pause channel: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: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:339stale, 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(instructingblockedafter 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.