Hardening the Agent Pool — From Design to Deployable
Continues from Agents Aren’t Connections, which covered why CLI agent sessions need suspend/resume semantics instead of connection pooling.
The design was right. The implementation had holes.
The last session landed a working agent pool — capacity-bounded, suspend/resume lifecycle, pressure-based eviction, identity-correlated instances. It compiled, the tests passed. But “it works on the happy path” is not the same as “it works.” An adversarial audit found seven issues, three of them correctness bugs that would have silently degraded the pool in production.
The pool leak nobody would have noticed
The most dangerous bug was in close(). When a worker finishes its task, the CaseHub engine calls close() on the agent session. The implementation recorded the final memory interaction — dutifully updating the eviction metadata — then returned. It never released the session back to the pool.
Every worker session that completed leaked one active slot. In a production scenario with repeated case execution — a research pipeline running 20 cases a day — the pool would exhaust its capacity ceiling within hours. No error, no warning. New workers would simply fail to provision with AgentPoolExhaustedException, and the operator would be staring at a full pool of sessions that are all “active” but doing nothing.
The fix is one line: sessionManager.suspendSession(managedSession.instanceId()) at the end of close(). The session transitions to SUSPENDED, the active slot is freed, and the conversation state persists on disk for potential resume. One line, but the kind of line that separates “demo” from “deployable.”
The eviction formula that didn’t match the design
The design spec said eviction should be multiplicative — memory amplifies idle time. The implementation was additive: idleSeconds + (memoryMB / 10). This meant a 500MB session idle for 1 second scored 51, while a 10MB session idle for 50 seconds scored 51 too. Memory barely registered for short idle periods.
The multiplicative formula — (idleSeconds + 1) × (1 + memoryMB / 100) — preserves idle time as the primary signal while giving memory-heavy sessions a proportional penalty. The +1 base ensures a just-created 500MB session doesn’t score zero. A session consuming 500MB of RAM is five times more expensive to keep around than a 10MB session, all else being equal — and the formula now reflects that.
Making the pool consumable
Fixing bugs is necessary. Making the pool usable by other projects is the actual goal. Claudony isn’t the only project that needs to manage agent sessions — scaffold, desiredstate, and ops all provision workers, and they’re about to integrate with this pool.
The pool needed three things before downstream projects could use it: documentation, CDI injectability, and API clarity.
CDI exposure was the critical piece. The AgentSessionManager was created internally by ClaudonyAgentBackend’s constructor — invisible to CDI. A downstream project wanting to acquire or release sessions had no injection point. A @Produces @ApplicationScoped method on the backend now makes the session manager injectable. One annotation, but it changes the pool from an implementation detail to a platform capability.
The consumer guide got a full Agent Pool Management section: lifecycle state machine, key classes table, eviction explanation, REST endpoint reference, CDI injection example, and config properties. A developer integrating the pool from scaffold can read the guide and know exactly what to inject, what methods to call, and what config knobs to tune.
Where this lands next
Each downstream project uses the pool differently:
scaffold provisions agent sessions for template generation — short-lived, one-shot tasks. It needs acquire/release with EXCLUSIVE working directory policy. The sessions are cheap, the pool turnover is high, and the default eviction formula works without tuning.
desiredstate runs reconciliation loops — long-lived agents that monitor and correct system state. These sessions are identity-sticky: a reconciler for the auth subsystem should resume with its prior conversation context, not start cold. The pool’s identity-correlated resume (match by identity + workingDir before creating new) handles this directly.
ops is the monitoring layer — it needs pool status (GET /api/agent-pools) to surface health in the operational dashboard. It also provisions diagnostic agents that investigate alerts, which means concurrent sessions on the same working directory under SHARED_READ policy.
The plan is to spike scaffold first. Try to integrate, discover which abstractions are right and which are wrong, then fix the pool with evidence before desiredstate and ops arrive. Two deferred issues track the gaps that might surface: a pluggable EvictionPolicy SPI (if any consumer needs custom scoring) and a test utilities JAR (once the downstream test pattern is clear).
I was wrong about the eviction formula in the first design session — additive instead of multiplicative. I was wrong about close() — it recorded metrics without releasing the slot. The audit found both. The lesson isn’t “audit everything” — it’s that the difference between a working design and a deployable one is the gap between the happy path and every other path. The pool works now. The next test is whether scaffold agrees.