Skip to content

feat(acp): session rotation on context window exhaustion#155

Merged
wesbillman merged 3 commits intomainfrom
feat/context-window-session-rotation
Mar 22, 2026
Merged

feat(acp): session rotation on context window exhaustion#155
wesbillman merged 3 commits intomainfrom
feat/context-window-session-rotation

Conversation

@wesbillman
Copy link
Collaborator

Summary

When an ACP agent hits MaxTokens or MaxTurnRequests (context window exhaustion), the session was previously returned to the pool unchanged — subsequent prompts would re-enter the bloated session and fail or degrade. This PR adds automatic session rotation to handle context window exhaustion gracefully.

Changes

Reactive Session Rotation (pool.rs)

  • When StopReason::MaxTokens or StopReason::MaxTurnRequests is returned, the channel session is invalidated so the next prompt creates a fresh one
  • Heartbeat sessions are also rotated on exhaustion

Proactive Turn-Based Rotation (config.rs, pool.rs, main.rs)

  • New config: SPROUT_ACP_MAX_TURNS_PER_SESSION (CLI: --max-turns-per-session, default: 0 = disabled)
  • When set, sessions are rotated after N successful turns, preventing context buildup before hitting the wall
  • Set to 1 for "fresh session per task" mode — agents use channel history (context_message_limit) as context instead of accumulating in-session state

Refactored Session Invalidation (pool.rs)

  • Extracted OwnedAgent::invalidate_session() and OwnedAgent::invalidate_all_sessions() helper methods
  • Replaced all scattered sessions.remove() / sessions.clear() / heartbeat_session = None patterns throughout run_prompt_task() with the new helpers
  • Turn counters are now consistently cleaned up across all invalidation paths (error, timeout, exit, rotation)

Logging

  • MaxTokens and MaxTurnRequests log messages now note upcoming session rotation
  • Session rotation events logged at info level with source and stop reason

Files Changed

  • crates/sprout-acp/src/pool.rs — rotation logic, turn counters, invalidation helpers, PromptContext field
  • crates/sprout-acp/src/config.rs — new CLI arg, Config field, summary string
  • crates/sprout-acp/src/main.rs — wired new fields into all 3 OwnedAgent construction sites and PromptContext

Testing

  • All 172 existing unit tests pass
  • Clean clippy, clean fmt
  • Pre-push CI (rust-fmt, rust-clippy, rust-tests, desktop-build, desktop-check, desktop-tauri-check) all pass
  • Backward compatible: default max_turns_per_session=0 preserves existing behavior exactly

wesbillman and others added 3 commits March 21, 2026 11:19
When an agent hits MaxTokens or MaxTurnRequests, the channel session is
now invalidated so the next prompt creates a fresh one instead of reusing
the bloated session.

Also adds proactive turn-based rotation via SPROUT_ACP_MAX_TURNS_PER_SESSION
(default 0 = disabled). When set, sessions are rotated after N successful
turns before hitting the context limit.

Refactored session invalidation into OwnedAgent::invalidate_session() and
invalidate_all_sessions() helpers for consistency across all code paths.

Changes:
- pool.rs: Session rotation logic in run_prompt_task Ok(Ok()) arm,
  turn counters on OwnedAgent, invalidation helpers, PromptContext field
- config.rs: New max_turns_per_session CLI arg and Config field
- main.rs: Wire PromptContext and OwnedAgent construction sites
* origin/main:
  feat: sprout-cli — agent-first CLI with full MCP parity (48 commands) (#158)
  fix(desktop): resolve composer cursor drift on multi-line input (#153)
Fix 4 post-prompt error paths that cleared sessions without clearing
turn counters, causing stale counts to leak across session lifetimes.

Extract SessionState from OwnedAgent so the session/turn-counter state
machine is testable without spawning a real agent subprocess. All session
mutation now goes through SessionState methods — no raw field access
outside the impl.

Methods:
- invalidate(source) — clear one channel or heartbeat
- invalidate_channel(id) -> bool — clear one channel, return whether it existed
- invalidate_all() — clear everything (agent exit)

Also fix: handle_prompt_result's channel-removal now uses
invalidate_channel() instead of raw retain().

8 unit tests cover all SessionState methods and edge cases.
@tlongwell-block
Copy link
Collaborator

Pushed a fix commit on top: 0e55e3d — consistent turn counter cleanup + SessionState extraction.

What changed:

The PR correctly migrated the pre-prompt error paths in run_prompt_task() to use invalidate_session() / invalidate_all_sessions(), but four post-prompt error paths still used raw sessions.clear() / sessions.remove() without clearing turn counters. After a post-prompt error, the replacement session would inherit a stale turn count and could rotate prematurely when max_turns_per_session is enabled.

The fix:

  • Extracted SessionState from OwnedAgent — holds sessions, turn_counts, heartbeat_session, heartbeat_turn_count and the invalidation methods. This separates connection state from session state and makes the state machine unit-testable without spawning a real agent subprocess.
  • Three methods on SessionState: invalidate(source), invalidate_channel(id) -> bool, invalidate_all(). All session mutation in pool.rs and main.rs now goes through these — zero raw field access outside the impl.
  • Fixed handle_prompt_result in main.rs: the removed_channels retain path was also missing turn counter cleanup. Now uses invalidate_channel() in a loop.
  • 8 unit tests covering all SessionState methods and edge cases.
  • 180 tests pass, clean clippy, clean fmt, all pre-push hooks green.

This comment was written by AI (goose).

@wesbillman wesbillman merged commit 9d6e82b into main Mar 22, 2026
8 checks passed
@wesbillman wesbillman deleted the feat/context-window-session-rotation branch March 22, 2026 15:59
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.

2 participants