Add concurrent agent limiting to kugetsu CLI

- Add MAX_CONCURRENT_AGENTS (default: 3) to limit concurrent agents
- Implement acquire_agent_slot() and release_agent_slot() with flock
- Wrap cmd_start, cmd_continue, and cmd_delegate with slot management
- cmd_delegate holds slot until background task completes (fire-and-forget + blocking)
- Add basic concurrency tests to test suite
This commit is contained in:
shokollm
2026-03-31 07:26:00 +00:00
parent dfc87e3da3
commit 3c15d8df1d
12 changed files with 652 additions and 4 deletions

View File

@@ -8,6 +8,56 @@ REPOS_CONFIG="$KUGETSU_DIR/repos.json"
INDEX_FILE="$KUGETSU_DIR/index.json"
NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json"
LOGS_DIR="$KUGETSU_DIR/logs"
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}"
AGENT_COUNT_FILE="$KUGETSU_DIR/.agent_count"
AGENT_LOCK_FILE="$KUGETSU_DIR/.agent_lock"
acquire_agent_slot() {
local timeout="${1:-300}"
local waited=0
(
flock -w 1 200 || { echo "Error: Could not acquire lock" >&2; exit 1; }
local count
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ "$count" -lt "$MAX_CONCURRENT_AGENTS" ]; then
echo $((count + 1)) > "$AGENT_COUNT_FILE"
exit 0
fi
exit 1
) 200>"$AGENT_LOCK_FILE"
local result=$?
if [ $result -ne 0 ]; then
local count
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ $waited -ge $timeout ]; then
echo "Error: Timeout waiting for agent slot (max: $MAX_CONCURRENT_AGENTS, current: $count)" >&2
fi
return 1
fi
return 0
}
release_agent_slot() {
(
flock -w 1 200 || true
local count
count=$(cat "$AGENT_COUNT_FILE" 2>/dev/null || echo 0)
if [ "$count" -gt 0 ]; then
echo $((count - 1)) > "$AGENT_COUNT_FILE"
fi
) 200>"$AGENT_LOCK_FILE"
}
run_with_limit() {
local log_file="$1"
shift
local cmd=("$@")
(
"${cmd[@]}" >> "$log_file" 2>&1
release_agent_slot
) &
disown
}
usage() {
cat << 'EOF'
@@ -83,6 +133,7 @@ EOF
ensure_dirs() {
mkdir -p "$SESSIONS_DIR"
[ -f "$AGENT_COUNT_FILE" ] || echo 0 > "$AGENT_COUNT_FILE"
}
ensure_worktree_dir() {
@@ -502,7 +553,11 @@ cmd_delegate() {
mkdir -p "$LOGS_DIR"
local log_file="$LOGS_DIR/delegate-$(date +%s).log"
nohup opencode run --continue --session "$pm_session" "$message" > "$log_file" 2>&1 &
if ! acquire_agent_slot; then
echo "Error: Max concurrent agents ($MAX_CONCURRENT_AGENTS) reached. Try again later." >&2
exit 1
fi
nohup sh -c "opencode run --continue --session '$pm_session' '$message' >> '$log_file' 2>&1; release_agent_slot" > /dev/null 2>&1 &
disown
echo "Delegated to PM agent (logged to $(basename "$log_file"))"
}
@@ -796,11 +851,19 @@ 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
remove_worktree_for_issue "$issue_ref"
exit 1
fi
trap release_agent_slot EXIT
if [ "$DEBUG_MODE" = true ]; then
opencode run --fork --session "$base_session_id" "$message" --workdir "$worktree_path" 2>&1 | tee "$SESSIONS_DIR/$session_file.debug.log"
else
opencode run --fork --session "$base_session_id" "$message" --workdir "$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=""
@@ -869,6 +932,11 @@ 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
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
echo "Using worktree: $worktree_path"
if [ "$DEBUG_MODE" = true ]; then
@@ -883,6 +951,8 @@ cmd_continue() {
opencode run --continue --session "$opencode_session_id" "$message"
fi
fi
release_agent_slot
trap - EXIT
}
cmd_list() {