Skip to content

No mid-session guardrail: a productive-but-looping session (265 turns, 49.7M tokens) is bounded only by the clock #158

Description

@ferrandmathis-afk

Version: bmad-loop 0.8.1 (uv tool, git install) · macOS 15 (Darwin 24.6.0) · git worktree isolation, adapter.name = "claude" (interactive tmux drive)
Type: feature / hardening · Related: #128 (token cap is advisory by design), #157 (the timeout that eventually stopped this session)

The gap

A single dev session took 265 assistant turns and burned 49.7M raw tokens (≈5.7M weighted) on one story before session_timeout_min finally cut it off at 90 minutes. Nothing stopped it earlier because, in the current architecture, nothing can: there is no bound on a session that keeps working — only on time, silence, or a crash.

An adapter.run session can end only as completed | stalled | timeout | crashed (adapters/base.py:53). Mapping those to what actually bounds a session:

  • timeout — timeout_s = session_timeout_min * 60. A wall-clock bound, not a work bound. 90 min was plenty of time to spend 49.7M.
  • stalled — governed by dev_stall_grace_s / dev_stall_nudges, and it only fires on an idle session. Per the policy doc, "pane output re-arms the grace window" — so a session emitting output continuously never even enters the grace countdown.
  • crashed — process death.

None of these observes how much a session is doing. A session that thrashes productively — re-reading, re-editing, second-guessing, 265 turns deep — is indistinguishable from a healthy one from outside the pane: output is flowing, no result-less Stop. The stall detector looks for silence, not for waste. max_tokens_per_story is the one signal that could catch it, but it's advisory-only and evaluated post-done (#128), so it never even ran here (the story timed out before reaching that path).

Net: the only guardrail on a runaway-but-busy session is the clock, and the clock doesn't care about turns or tokens.

Why it's structural, not just a missing check

The claude adapter drives Claude Code interactively over tmux, not headless — so claude -p --max-turns N isn't in play, and the orchestrator supervises from outside the pane. It can't count the model's internal agentic turns because it never sees them as discrete events; it sees a stream of pane output and occasional Stop hooks. So "cap the turns" can't just be a config check bolted onto the existing loop — it needs a real signal.

Proposed direction

A mid-session budget guard, evaluated while the session runs rather than only at done:

  1. Sample cumulative session tokens (the adapter already tallies usage — the run's state.json carried per-session input/output/cache_read/cache_creation) on each pane-output poll.
  2. When weighted spend crosses a configurable limits.max_tokens_per_session (or reuse max_tokens_per_story once it's enforcement-backed per the v7 plan noted in max_tokens_per_story is never enforced and its breach event is invisible #128), take a real action: nudge-to-wrap-up → then terminate as a distinct over_budget status → defer/escalate per policy.
  3. Emit the event when the guard trips, and mirror it into ATTENTION, so a 49.7M session is visible as it happens, not reconstructed from a transcript afterward.

A turn-count cap (limits.max_turns_per_session) would be a cheaper proxy if per-turn Stop/output events can be counted, but a token budget is the more honest bound given the cost is quadratic in accumulated context.

Notes

  • I'm aware max_tokens_per_story is never enforced and its breach event is invisible #128 was closed with "token-per-story is advisory for now, real guards coming in v7." Filing this so the concrete failure mode — a productive session that no existing guard can see — is on record for that work.
  • Run directory has since been deleted; the 265-turn / 49.7M / 90-min figures are from the session transcript and journal before deletion.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions