Skip to content

session: the daily spend ceiling binds task and errand turns too (#1546) - #1842

Open
7vignesh wants to merge 3 commits into
Agent-Field:devfrom
7vignesh:fix/1546-daily-ceiling-every-path
Open

7vignesh wants to merge 3 commits into
Agent-Field:devfrom
7vignesh:fix/1546-daily-ceiling-every-path

Conversation

@7vignesh

@7vignesh 7vignesh commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What changed

The daily spend ceiling (CODEAF_DAILY_BUDGET, the machine-wide limit over a day) was not enforced on every execution path. Agent.railBlockLocked skipped the daily check for an InTask runner (a task node, including one under --one-model) and the home Errand, and the crew guard that also enforces the day cap is absent under --one-model and for an InTask agent with no crew. So a chat /task or a task under --one-model kept spending past the day's limit with no refusal, while headless codeaf do and standing firings honoured it (#1546).

The remaining gap after #1627, which enabled the chat/crew daily check but kept the InTask/Errand carve-out (noted as still-open in that PR's review).

The fix removes the carve-out in internal/session/rail.go: the daily ceiling is now checked on every turn-starting path (every caller of railBlockLocked - chat submit, resume, harness, image, team-wake, and the task node runner). It is a ceiling READ of the shared ledger, never a tally, so a path already held to the crew's own day cap is refused in two places and never charged twice. The rail still never cuts a turn in half - it stops the next turn, exactly as the conversation rail does.

How it was checked

Verified with real runs in a Linux container (golang:1.26), since internal/session does not test-build on my Windows box (it transitively imports the //go:build !windows internal/seniordev/engine/steploop):

  • go test ./internal/session/ -run TestTheDailyCeilingBindsTaskAndErrandTurnsToo - new test, passes for all three postures: an InTask task node runner, a task node under --one-model, and the home Errand, each refused past the ceiling with daily limit reached, and a task turn under the ceiling still allowed. It FAILS on all three with the carve-out restored (verified by reverting), so it is a real regression guard.
  • go test ./internal/session/ -run TestSpendRailRefusesTurnWhenDailyBudgetExceeded and the full Rail|Spend|Budget|Daily set - all pass.
  • go build ./internal/session/ on Windows and GOOS=linux go test -c ./internal/session/ both succeed.

Pre-existing failures on clean dev (unrelated to this change, verified by stashing): TestTheTaskPromptTeachesTheMeasureIsTheLoop, TestTheFixedPrefixStaysUnderItsBudget, TestTheLeanPrefixStaysUnderItsBudget - all about system-prompt text and byte budgets, not spend.

Checklist

  • A change entry - docs/changes/unreleased/1842-daily-ceiling-every-path.md.
  • The manual knows about it - no slash command, key, tool, default, limit or refusal moved; the daily limit and its /budget day wording are unchanged, this only extends which paths honour an existing limit. The manual already says new work waits at the daily limit.
  • No new line in .github/known-red.txt.
  • Only my own paths are staged.

Closes #1546.

…nt-Field#1546)

railBlockLocked skipped the daily budget check for InTask runners and the
home Errand, and the crew guard that also enforces it is absent under
--one-model and for an InTask agent with no crew. So a chat /task past the
day's limit, and a task under --one-model, kept spending with no refusal
while headless codeaf do and standing firings honoured the limit.

Remove the InTask/Errand carve-out so the daily ceiling is checked on every
turn-starting path. It is a ceiling read of the shared ledger, never a
tally, so a path already held to the crew's own day cap is refused in two
places, never charged twice.

@ZeroPoint95 ZeroPoint95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @7vignesh, thanks for closing the ordinary task and errand budget gap. Two cases still need addressing around internal/session/rail.go:75–83:

  1. Preserve the selected daily-budget profile in workers.

    Both task construction paths omit ProfileDir, so the new check reads the default profile. With a selected limit of $1,000 and $600 spent, a worker incorrectly refuses at the default $500. With a selected $1 limit and $1.50 spent, it still runs.

    The first case is a new regression; the second leaves the existing bypass unresolved. Please carry the selected budget authority into workers without enabling settings changes. Add regressions through both the production task-child constructor and current run-worker factory: the higher selected limit must allow dispatch, and the lower selected limit must refuse before any provider call.

  2. Preserve explicit spending preauthorization.

    codeaf do --yes-spend and CODEAF_PREAUTHORIZE_SPEND=1 deliberately allow spending past the daily limit, but the new worker check applies that limit again.

    An actual run worker with preauthorization, a $1 daily limit and $1.50 spent now stops before calling its provider; the exact base proceeds. Please propagate that authorization into the daily check and cover ordinary refusal alongside preauthorized execution. Separate task limits should remain in force.

These requests preserve the existing turn-admission policy; no per-call budget redesign is needed.

…ation into workers

ZeroPoint95's review on Agent-Field#1842 caught that the daily check read the default
profile for workers (which carry no ProfileDir), so a worker refused under a
higher selected limit and spent past a lower one, and that it re-applied the
limit --yes-spend deliberately lifts.

- Resolve the daily ceiling once at the root (where ProfileDir is set) and
  carry it as Config.DailyBudgetUSD; dailyCeilingUSD reads live from the
  profile for a root and the carried figure for a worker, never a profile at
  an empty path.
- Carry Config.SpendPreauthorized (--yes-spend / CODEAF_PREAUTHORIZE_SPEND=1)
  into workers; it lifts the daily check and nothing else, matching the crew
  guard.
- Thread both through all three worker constructors: the task-child
  (task_run.go), the adaptive-run worker (orchestrate.go) and the plan-run
  bash worker (run.CrewFactory, which gains a preauthorized parameter).

Adds regressions: a worker held to its carried selected budget (higher allows,
lower refuses) not the default, and preauthorization lifting the daily ceiling
while a conversation limit stays in force.
@7vignesh

Copy link
Copy Markdown
Contributor Author

Thanks @ZeroPoint95, both real. The daily check read the default profile for workers and ignored preauthorization. Fixed by resolving both at the root and carrying them down, since a worker is handed no ProfileDir (it would open the settings door).

1. The selected budget travels to workers.

  • Agent.dailyCeilingUSD reads the profile live for a root (ProfileDir set, so a /budget day change still takes effect next turn) and the carried Config.DailyBudgetUSD for a worker. It never reads a profile at an empty path, which is where the default $500 came from.
  • The resolved figure is carried through all three worker constructors: the task-child (task_run.go), the adaptive-run worker (orchestrate.go), and the plan-run bash worker (run.CrewFactory, which gains a preauthorized parameter and resolves the budget once from the run's profile).
  • Regression TestAWorkerIsHeldToTheSelectedBudgetItCarriesNotTheDefault: a worker carrying $1 with $1.50 spent refuses; a worker carrying $1000 with $0.60 spent runs (the case that wrongly refused at the default); a worker carrying nothing is held to no ceiling.

2. Preauthorization is honoured.

  • Config.SpendPreauthorized (--yes-spend / CODEAF_PREAUTHORIZE_SPEND=1) is carried into workers and lifts the daily check in railBlockLocked, the same authority doSpendGuard lifts from the crew guard. A /task from interactive chat carries false (that flag is headless-only).
  • Regression TestSpendPreauthorizationLiftsTheDailyCeiling: a preauthorized worker past the daily ceiling runs; and a separate conversation limit ($0.50 cap, $0.60 spent) still refuses under preauthorization, so the lift is the day's limit alone.

Verified by running internal/session and internal/run in a golang:1.26 Linux container (the session package does not test-build on my Windows box, which is //go:build !windows for steploop): both packages pass, including the five rail/budget tests and the full internal/run suite. go build ./... and go vet on the touched packages are clean.

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.

session: interactive chat bypasses daily spending limit

2 participants