Skip to content

buzz-acp: bound ACP session + provider-resource lifecycle (no eviction today) #6958

Open

Description

@salman1993

Background

Split out of the thread-scoped ACP sessions PR (#6732) review (reviewer P1).
Not introduced by that PR — it is a pre-existing lifecycle-hardening concern
that thread scoping makes more visible because it multiplies the number of
live sessions (one per thread instead of one per channel).

Problem

Sessions are retained indefinitely:

  • The pool's per-scope maps in buzz-acp (sessions, turn_counts,
    core_sections, canvas_sections, deliveries, session_owners) have no
    time- or size-based eviction. They only shrink on explicit triggers
    (!rotate, channel/membership removal, agent exit/panic). Under thread
    policy a busy channel can accumulate one entry set per thread and never
    release them.
  • buzz-agent sessions can retain their McpRegistry resources. A finite
    BUZZ_AGENT_MAX_SESSIONS exists but defaults to unlimited.
  • ACP has no obvious per-session close contract, so deleting only the
    Buzz-side session IDs would not reliably release provider-side resources
    (subprocesses, MCP servers, model context).

Why it isn't a clean PR-local fix

A partial eviction (dropping Buzz-side IDs) would leak provider resources
and give a false sense of boundedness. This needs a designed lifecycle:

  • an idle-TTL and/or LRU cap on pool scope maps, keyed by SessionScope,
  • a real per-session close/teardown contract that releases provider resources
    (ACP session close, McpRegistry teardown),
  • a sensible finite default for BUZZ_AGENT_MAX_SESSIONS,
  • metrics/telemetry for live-session count and evictions.

Acceptance (proposal)

  • Bounded live sessions per agent under sustained multi-thread load.
  • Evicting a session provably releases provider-side resources.
  • Defaults are safe for the default channel policy (no behavior change) and
    bound growth under thread policy.

References

Activity

  1. rsaulo commented on Sep 3, 2026

    @rsaulo

    Design proposal: bound sessions on SessionScope with idle-TTL + LRU cap, using session/close as the teardown contract

    One update to the third bullet first, since it changes what is available: ACP stabilized session/close on 2026-04-23 (RFD "Session Close" → Completed; now in the v1 spec under Session Setup). Agents advertise it as agentCapabilities.sessionCapabilities.close in initialize, and clients MUST NOT call it otherwise. So a per-session teardown contract does exist now, and the "partial eviction leaks provider resources" objection has an answer for adapters that advertise it. session/close also requires the agent to cancel ongoing work first, which is what makes eviction safe to express at all.

    Activity clock. last_active: HashMap<SessionScope, Instant>, touched on session creation and on every completed turn. A scope with no entry sorts as "idle forever", so a bookkeeping gap gets reclaimed instead of becoming an immortal session.

    Two triggers, unioned, LRU order. (a) idle past idle_ttl; (b) once a worker holds more than max_live_sessions, the least-recently-used ones beyond the cap. The TTL bounds what a forgotten thread costs; the cap bounds a burst of threads arriving faster than the TTL can retire them. Both evict oldest-idle first.

    Where it runs, and why in-flight turns are safe. In the existing 30s maintenance tick, over workers currently sitting idle in AgentPool::agents only. A checked-out worker's SessionState is out of the pool's hands for the duration of the turn, so an active turn cannot have its session closed under it — the safety property falls out of the existing ownership model rather than needing new locking.

    Close before forget. Send session/close first; drop the scope's entries (sessions, turn_counts, core_sections, canvas_sections, deliveries, plus the session_owners directory entry, or affinity routes to a dead session) only after the adapter acknowledges. On error or timeout, keep local state and retry next sweep — dropping the Buzz-side ID first is precisely what strands the remote session with no handle left to close it. !rotate and invalidation move IDs into the same retry set instead of leaking. A per-sweep close budget plus a rotating candidate cursor keep one wedged session from starving its siblings or stalling the loop.

    Adapters without the capability. Skip eviction entirely and warn once at startup. Silently dropping IDs there is strictly worse than keeping them (#2961 measures why: the old tree stays resident and the next prompt spawns another). The only remaining lever is recycling the worker process — #2961's option 1 — which should stay explicit, logged, and rare.

    Why BUZZ_ACP_IDLE_POOL_SLEEP does not cover this. It is all-or-nothing on the pool, and only when the whole harness is quiet. An agent with one hot thread and twenty cold ones never qualifies, and that is exactly the shape that fails.

    Knobs, opt-in the way #6732 shipped SESSION_POLICY=channel: BUZZ_ACP_SESSION_IDLE_TTL_SECS and BUZZ_ACP_MAX_LIVE_SESSIONS, 0 disables either. Off by default preserves today's behavior byte for byte; flipping the default per harness is a separate, canary-able decision.

    Metrics, per the issue's acceptance list: live sessions per worker, evictions by reason (ttl / cap), close failures, and skipped-uncloseable adapters.

    Field data. We run 9 managed agents in production under thread policy. On claude-agent-acp, three closes took one worker's child tree from 9 → 6 → 3 → 0 processes and 1437 MB → 96 MB. Per-session cost differs by roughly three orders of magnitude across adapters (claude-agent-acp spawns a CLI per session, ~535 MB; codex-acp shares a core and pays ~200 MB of MCP servers; opencode keeps sessions in-process at ~0.7 MB), which is why we run per-harness defaults rather than one number — 10 min / 3 for claude, 30 min / 12 for codex, 2 h / 64 in-process.

    What we would want decided before code:

    1. Default posture — off everywhere, or per-harness defaults under thread only?
    2. Per-harness defaults keyed on the initialize agent name at all, or a single global knob operators tune themselves?
    3. Does eviction apply to Conversation scopes (incl. DMs) or thread scopes only?
    4. Is the finite BUZZ_AGENT_MAX_SESSIONS default this ticket or a separate one?
    5. Is perceived context loss acceptable silently, or does a closed thread owe the channel a notice? (session/load / sessionCapabilities.resume is a possible third path: restore instead of restart.)

    We have a version of this running in our fork and can share measurements, traces, or a patch for whichever shape you pick — happy to follow your call on the questions above before writing anything upstream.

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