diff --git a/docs/agent-concurrency-benchmark.md b/docs/agent-concurrency-benchmark.md new file mode 100644 index 0000000..36a1276 --- /dev/null +++ b/docs/agent-concurrency-benchmark.md @@ -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 +``` + +## 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 ` - destroy specific session +- `kugetsu destroy --pm-agent -y` - destroy PM agent +- PM should destroy sessions after PR merged (on natural breakpoints) diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 9b876bd..6a12dab 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -48,6 +48,21 @@ release_agent_slot() { ) 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() { local log_file="$1" shift @@ -852,19 +867,21 @@ cmd_start() { local before_set="${before_sessions//$'\n'/|}" 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" exit 1 fi - trap release_agent_slot EXIT + 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 - 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 - release_agent_slot - trap - EXIT local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort) 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 "") echo "Continuing session for '$session_name'..." - if ! acquire_agent_slot; then - echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2 - exit 1 - fi - trap release_agent_slot EXIT + # Note: --continue always allowed (existing sessions don't count toward limit) if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then echo "Using worktree: $worktree_path" 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 - 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 else 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 - opencode run "$message" --continue --session "$opencode_session_id" + opencode run "$message" --continue --session "$opencode_session_id" 2>&1 & fi fi - release_agent_slot - trap - EXIT } cmd_list() {