fix(dispatcher+watcher): never auto-dispatch into dedicated sessions

Observed: tasks from filesecure/.agent-queue/inbox and SecuScan/
.agent-queue/inbox were being routed into ccl-1-conformvault and
ccl-2-scanyze whenever those sessions happened to be idle. Those are
the operator's manual interactive Claude sessions, not dispatch
targets — the auto-dispatch was (a) hijacking a Claude instance the
operator was using and (b) triggering /exit via the watcher's
completion path when the side-task finished, kicking the operator out
mid-conversation.

findFreeSession was iterating Pool.Dedicated before the autonomous
pool, so any idle dedicated session was the first candidate.

- Dispatcher.findFreeSession: remove the Dedicated loop entirely.
  Auto-dispatch is now pool-only (ccl-auto-11..20).
- Watcher.completeSession: defense-in-depth — even if a dedicated
  session ever ends up in "working" state, it is no longer /exit'd;
  just marked idle. Pool /exit behaviour unchanged (context recycle).
- Tests: new TestFindFreeSessionSkipsDedicated proves the routing;
  3 existing tests rewritten to use the autonomous pool instead of
  relying on Dedicated as a fake pool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-16 13:30:26 +00:00
parent 6b109ed1bc
commit 4cbdcf143a
4 changed files with 136 additions and 29 deletions

View file

@ -140,14 +140,13 @@ func (d *Dispatcher) dispatchProject(inboxDir string) {
}
}
// findFreeSession returns the name of an idle, live, cooldown-free session.
// Returns "" if no session is available.
// findFreeSession returns the name of an idle, live, cooldown-free session
// from the autonomous pool. Dedicated sessions are intentionally NOT
// considered: those host the operator's manual interactive work. Routing a
// background dispatch into them would (a) hijack a Claude instance the
// operator is using and (b) trigger the watcher's /exit recycle at task
// end, kicking the operator out mid-conversation.
func (d *Dispatcher) findFreeSession() string {
for _, ds := range d.config.Pool.Dedicated {
if d.isSessionFree(ds.Name) {
return ds.Name
}
}
prefix := d.config.Pool.Autonomous.Prefix
if prefix == "" {
prefix = "ccl-auto-"