You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
limits.max_tokens_per_story (default 2M, policy.py:143) is read in exactly one place, and that place runs after the story is already done. A story that defers or escalates — the outcome in the umbrella field report — is never checked at all. The reporter burned 8.35M weighted tokens against a 2M cap and learned about it from nothing.
Evidence (validated at 3c68153)
Sole read site:engine.py:1768-1775, inside _finalize_commit_phase, afteradvance(task, Phase.DONE) (engine.py:1764). It appends one token-budget-exceeded journal line and does nothing else — no notify, no state.
Zero consumers of that journal kind anywhere. The TUI and status show totals only, never budget or headroom (cli.py:1567-1580, tui/widgets.py:165-177).
Worse than "late": the check lives only on the commit path. A story that defers or escalates gets no entry whatsoever. Total silence, not late silence.
core.toml:144-148 is missing label and description — the only limits field without them.
Fix outline
No new knob — the cap is advisory by contract (docs/FEATURES.md:145); warn at 100%.
Delete the post-done check (engine.py:1768-1775). Add _note_story_token_budget(task) called from _run_session right after self._save() (engine.py:2709), where task.tokens is both story-cumulative and fresh. Sweep and Stories engines inherit it for free.
Would have fired after session 1–2 of the reported run.
Reuse the token-budget-exceeded journal kind (zero src/ consumers) and add a budget field. gates.notify title story token budget exceeded: <key>, message carrying weighted / cap / raw plus "advisory: story continues".
Persisted latch StoryTask.token_budget_warned: bool = False — resume must not re-notify.
Weight comes from live policy limits.cache_read_weight (enforcement side of the doctrine comment at engine.py:615-623; display uses the persisted RunState.cache_read_weight() snapshot, model.py:399).
core.toml:144-148: add the missing label + description (presentation-only, no sync-test impact).
Budget headroom display in status and the TUI (free, independent of the above).
Tests
Amend test_engine.py:1235-1264 (still exactly one journal entry, weighted = 160000, plus ATTENTION, the budget field, and the latch); keep :1065-1098 untouched as the no-false-positive case; new warns-on-a-deferring-story test (ablation: re-guard with phase == DONE → it must fail); warns-once test (ablate the latch); latch-survives-resume test (ablate to_dict); test_model.py round-trip. Settings sync tests must pass unchanged.
Edge cases already analysed: resume-replay sessions don't hit the new site (the persisted latch covers it); aborted sessions skip judgment; sweep bundles inherit the behaviour.
Part of the 0.9.0 field-report umbrella (linked parent).
limits.max_tokens_per_story(default 2M,policy.py:143) is read in exactly one place, and that place runs after the story is already done. A story that defers or escalates — the outcome in the umbrella field report — is never checked at all. The reporter burned 8.35M weighted tokens against a 2M cap and learned about it from nothing.Evidence (validated at
3c68153)engine.py:1768-1775, inside_finalize_commit_phase, afteradvance(task, Phase.DONE)(engine.py:1764). It appends onetoken-budget-exceededjournal line and does nothing else — no notify, no state.statusshow totals only, never budget or headroom (cli.py:1567-1580,tui/widgets.py:165-177).max_tokens_per_session, default 4M,adapters/generic.py:605-683) is a different cap, latched per session, and blind to cross-session story accumulation. Nothing budget-related has landed since 0.9.0.core.toml:144-148is missinglabelanddescription— the onlylimitsfield without them.Fix outline
docs/FEATURES.md:145); warn at 100%.engine.py:1768-1775). Add_note_story_token_budget(task)called from_run_sessionright afterself._save()(engine.py:2709), wheretask.tokensis both story-cumulative and fresh. Sweep and Stories engines inherit it for free.token-budget-exceededjournal kind (zerosrc/consumers) and add abudgetfield.gates.notifytitlestory token budget exceeded: <key>, message carrying weighted / cap / raw plus "advisory: story continues".StoryTask.token_budget_warned: bool = False— resume must not re-notify.limits.cache_read_weight(enforcement side of the doctrine comment atengine.py:615-623; display uses the persistedRunState.cache_read_weight()snapshot,model.py:399).core.toml:144-148: add the missinglabel+description(presentation-only, no sync-test impact).FEATURES.md:145andREADME.md:593.SessionSpec,generic.py,opencode_http.pyuntouched.Explicit non-goals (follow-ups)
story_token_budget/story_tokens_spenttoSessionSpec(adapters/base.py:75-78), populate atengine.py:2546-2549, and extend the trip condition ingeneric.py:608-614+opencode_http.py:1067-1073— needs a latch separate from No mid-session guardrail: a productive-but-looping session (265 turns, 49.7M tokens) is bounded only by the clock #158's singlebudget_trippedboolean.statusand the TUI (free, independent of the above).Tests
Amend
test_engine.py:1235-1264(still exactly one journal entry, weighted = 160000, plus ATTENTION, thebudgetfield, and the latch); keep:1065-1098untouched as the no-false-positive case; new warns-on-a-deferring-story test (ablation: re-guard withphase == DONE→ it must fail); warns-once test (ablate the latch); latch-survives-resume test (ablateto_dict);test_model.pyround-trip. Settings sync tests must pass unchanged.Edge cases already analysed: resume-replay sessions don't hit the new site (the persisted latch covers it); aborted sessions skip judgment; sweep bundles inherit the behaviour.
Part of the 0.9.0 field-report umbrella (linked parent).