Skip to content

fix(goal): spend the budget per judged turn and never leave a goal active on an idle session - #208

Merged
filipeforattini merged 1 commit into
mainfrom
goal-turn-budget
Sep 11, 2026
Merged

fix(goal): spend the budget per judged turn and never leave a goal active on an idle session#208
filipeforattini merged 1 commit into
mainfrom
goal-turn-budget

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Sep 11, 2026

Copy link
Copy Markdown

Problem

Four defects in goal mode (/goal) left the budget meaningless or the goal stranded:

  1. Budget unit. GoalRuntime.beginTurn ran before every provider attempt — each tool round-trip and each retry — and charged one turn each time, so the default of 20 was exhausted by a goal that read fifteen files and ran five commands. Meanwhile the goal block said "Turn 3 of 20", the continuation said "turn N of M", the TUI toast said "20 turns" and the budget dialog said "provider attempt budget".
  2. CAS failure ended the turn silently. /goal-budget and /goal-resume bump updated; if that landed between the fresh read in prompt.ts and the compare in afterTurn, afterTurn returned undefined, the loop broke, nothing was recorded and the goal stayed active on an idle session.
  3. Step ceiling and stall left the goal active with no trace. The step-budget stop broke out of the loop before afterTurn; the stall watchdog called state.cancel rather than SessionPrompt.cancel, bypassing goals.pause.
  4. Evidence and truncation. Observed tool output was collected since goal.created, so one read on turn 1 satisfied evidence.length > 0 forever; and [...gates, ...observed].join().slice(-24000) cut gate results first.

Fix

  • A turn is one full agent turn ending in a judge cycle. turns.used is incremented in SessionGoal.apply (once per judged turn, never on WAIT); beginTurn is an admission gate only. decide treats the turn being judged as spending one, so the budget check is used + 1 >= max. The judge prompt, budgetReason, the TUI budget dialog, the resume toast and specs/goal-modes.md all say "turns". Default stays 20; runaway protection remains the retry policy, loop guard, step ceiling and MAX_JUDGE_FAILURES.
  • afterTurn takes the decision on a record and writes it only if that record is still stored. On a lost race it re-reads and decides again on the fresh record (a raised budget can turn a stop back into a continue); a second loss logs and pauses the goal with a reason.
  • The step-ceiling stop and the stall watchdog both pause the goal through goals.pause with their own reason (stopped at the step ceiling after N steps; ..., stalled: no output for ...) before ending the turn.
  • Evidence is scoped to the current turn: everything since the last spent turn (goal.judged, new optional field, falling back to goal.created). Anchoring on the last spent turn rather than the last user message keeps a turn parked on background work and the report that re-enters it as one turn. SessionGoal.evidence puts gate results last so the tail cut keeps them.

Test

Regression tests fail on main and pass here (run with the source stashed and the new tests in place, then again after restoring):

  • test/session/goal.test.ts: budget check (used: 2, max: 3 is the last turn), apply spends one per judged turn and none on WAIT and stamps judged, evidence keeps gate results through truncation — 3 fail before (SessionGoal.evidence is not a function, wrong counts), 20/20 pass after.
  • test/session/prompt.test.ts: retry inside a turn spends nothing (before: retry refused, 1 hit); many tool steps are one judged turn (before: paused after the first attempt); terminal failure spends 0; background WAIT spends 0; /goal-budget landing during judgement is recorded against the fresh record (before: active); step ceiling pauses the goal (before: active); stall pauses the goal (before: active); a tool result from an earlier turn is not evidence for a later claim.
  • Whole test/session directory green; bun run typecheck clean in packages/redcode and packages/tui.

Not done: packages/sdk/openapi.json / types.gen.ts not regenerated for the optional judged field (the build script regenerates the whole client; no client reads the field). Spec line about SessionV2 charging per provider attempt left as is — that is a different runtime in packages/core. The uncommitted .changeset/repair-goal-attempts.md in the main checkout says retries count against budgets; this PR supersedes that behaviour and does not touch that file.

https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…tive on an idle session

A goal turn is one full agent turn ending in a judge cycle. `beginTurn` ran
before every provider attempt — each tool round-trip, each retry — and
charged one turn each time, so the default budget of 20 was gone after a turn
that read fifteen files, while every surface still spoke of turns. The
counter now moves in `SessionGoal.apply`, once per judged turn and never on a
WAIT; `beginTurn` only admits. The judge prompt, the budget reason, the TUI
dialog and toasts and the spec all say "turns".

Three ways a goal was left active with nothing recorded are closed. A
`/goal-budget` or `/goal-resume` landing while the judge decided made the
compare in `afterTurn` fail and the turn end silently; the decision is now
taken again on the fresh record, and a second loss pauses the goal with a
reason. The step-ceiling stop broke out before `afterTurn`, and the stall
watchdog cancelled through `state.cancel` without pausing; both now pause
the goal with their own reason.

Evidence for the judge is scoped to the current turn — everything since the
last spent turn (`goal.judged`), so a `read` on turn one no longer stands as
evidence for every later claim, while a turn parked on background work and
the report that re-enters it stay one turn. Gate results are placed last so
the tail cut keeps them.

Claude-Session: https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS
@blacksmith-sh

blacksmith-sh Bot commented Sep 11, 2026

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
e2e\regression\session-rename.spec.ts/
renames and closes the session tab from its context menu
View Logs
e2e\user-story\model-selection-flow.spec.ts/
creates a session in a new project, connects OpenCode Go, and selects its model
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@filipeforattini
filipeforattini merged commit ea93d05 into main Sep 11, 2026
11 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant