Merge pull request 'fix: implement session-counting for MAX_CONCURRENT_AGENTS limit (fixes #63)' (#65) from fix/issue-63-session-counting into main

This commit was merged in pull request #65.
This commit is contained in:
2026-04-01 07:40:00 +02:00
2 changed files with 152 additions and 18 deletions

View File

@@ -0,0 +1,123 @@
# Agent Concurrency Benchmark
**Date:** 2026-04-01
**Hardware:** 8GB RAM, 16 CPU cores
## Test Results
| Limit (PM+Dev) | Status | Rejection Test | Notes |
|----------------|--------|---------------|-------|
| 1 | ✓ Works | 1 dev rejected (PM=1, at limit) | Too strict for normal use |
| 3 | ✓ Works | 4th dev rejected (PM + 3 devs = 4, at limit) | Recommended |
| 5 | ✓ Works | 6th dev rejected (PM + 5 devs = 6, at limit) | Works, monitor memory |
## Architecture
OpenCode is a **cloud client** - agents run on OpenCode's server (MiniMax), not locally.
```
┌─────────────────┐ ┌─────────────────┐
│ Local Host │ │ OpenCode │
│ │ HTTPS │ Server │
│ kugetsu CLI │◄───────►│ (MiniMax) │
│ worktrees/ │ API │ Agents run │
│ sessions/ │ Key │ here │
│ opencode.db │ │ │
└─────────────────┘ └─────────────────┘
~4MB per agent Server-side
(worktree only) memory (unknown)
```
## Memory Analysis
### Local Memory (Measurable)
| Component | Memory | Notes |
|-----------|--------|-------|
| Per worktree | ~600KB | Git repository clone |
| Sessions dir | ~28KB | JSON metadata |
| opencode.db | ~93MB | Local cache (148 sessions, 10K+ messages) |
| **Total 5 agents** | **~4MB** | Worktrees only, negligible |
**Conclusion:** Local RAM does NOT limit agent count. A 1GB or 2GB system can run MAX=10 agents.
### Server Memory (Not Measurable)
- OpenCode server runs on MiniMax's infrastructure
- No local process to measure RSS/memory
- Agent computation happens server-side
- Memory limit determined by OpenCode service, not local hardware
### Local Bottleneck
The only local constraint is `MAX_CONCURRENT_AGENTS` limit, which:
- Counts session files (PM + dev agents)
- Enforced in kugetsu before spawning
- Prevents resource overload on OpenCode server
## Behavior
With MAX_CONCURRENT_AGENTS=N:
- PM agent counts toward the limit (along with all dev agents)
- At limit: NEW sessions are REJECTED
- Existing sessions can ALWAYS be continued (--continue doesn't count toward limit)
- PM is still accessible when at limit (user can wait or cancel tasks)
## Configuration
Default limit is set to **5 concurrent agents** in `skills/kugetsu/scripts/kugetsu`:
```bash
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-5}"
```
The limit can be overridden via environment variable:
```bash
MAX_CONCURRENT_AGENTS=3 kugetsu start <issue> <message>
```
## Implementation
Session counting approach (vs broken slot mechanism):
```bash
# Count all session files except base.json
count_active_dev_sessions() {
local count=0
if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local filename=$(basename "$session_file")
if [ "$filename" != "base.json" ]; then
count=$((count + 1))
fi
fi
done
fi
echo "$count"
}
```
## Session Files
```
~/.kugetsu/sessions/
base.json - base session (NOT counted)
pm-agent.json - PM agent (COUNTED)
github.com-user-repo#1.json - dev agent (COUNTED)
github.com-user-repo#2.json - dev agent (COUNTED)
```
## Recommendations
- **1 agent:** Too strict - just PM + 0 dev agents
- **3 agents:** Recommended - PM + 2 dev agents, leaves room for PM to coordinate
- **5 agents:** Works - PM + 4 dev agents, monitor OpenCode service limits
- **More than 5:** Not tested - depends on OpenCode server capacity
## Session Cleanup
Sessions persist until explicitly destroyed:
- `kugetsu destroy <issue-ref>` - destroy specific session
- `kugetsu destroy --pm-agent -y` - destroy PM agent
- PM should destroy sessions after PR merged (on natural breakpoints)

View File

@@ -48,6 +48,21 @@ release_agent_slot() {
) 200>"$AGENT_LOCK_FILE" ) 200>"$AGENT_LOCK_FILE"
} }
count_active_dev_sessions() {
local count=0
if [ -d "$SESSIONS_DIR" ]; then
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local filename=$(basename "$session_file")
if [ "$filename" != "base.json" ]; then
count=$((count + 1))
fi
fi
done
fi
echo "$count"
}
run_with_limit() { run_with_limit() {
local log_file="$1" local log_file="$1"
shift shift
@@ -852,19 +867,21 @@ cmd_start() {
local before_set="${before_sessions//$'\n'/|}" local before_set="${before_sessions//$'\n'/|}"
echo "Forking session for '$issue_ref'..." echo "Forking session for '$issue_ref'..."
if ! acquire_agent_slot; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2 # Session-counting: count actual dev sessions, reject if at limit
local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "$MAX_CONCURRENT_AGENTS" ]; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached" >&2
echo "Active sessions: $active_count" >&2
remove_worktree_for_issue "$issue_ref" remove_worktree_for_issue "$issue_ref"
exit 1 exit 1
fi fi
trap release_agent_slot EXIT
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log" opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log" &
else else
opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 opencode run "$message" --fork --session "$base_session_id" --dir "$worktree_path" 2>&1 &
fi fi
release_agent_slot
trap - EXIT
local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
local new_session_id="" local new_session_id=""
@@ -933,27 +950,21 @@ cmd_continue() {
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
echo "Continuing session for '$session_name'..." echo "Continuing session for '$session_name'..."
if ! acquire_agent_slot; then # Note: --continue always allowed (existing sessions don't count toward limit)
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
exit 1
fi
trap release_agent_slot EXIT
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
echo "Using worktree: $worktree_path" echo "Using worktree: $worktree_path"
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 | tee "$session_path.debug.log" &
else else
opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" opencode run "$message" --continue --session "$opencode_session_id" --dir "$worktree_path" 2>&1 &
fi fi
else else
if [ "$DEBUG_MODE" = true ]; then if [ "$DEBUG_MODE" = true ]; then
opencode run "$message" --continue --session "$opencode_session_id" 2>&1 | tee "$session_path.debug.log" opencode run "$message" --continue --session "$opencode_session_id" 2>&1 | tee "$session_path.debug.log" &
else else
opencode run "$message" --continue --session "$opencode_session_id" opencode run "$message" --continue --session "$opencode_session_id" 2>&1 &
fi fi
fi fi
release_agent_slot
trap - EXIT
} }
cmd_list() { cmd_list() {