From 1fbb96bd8d46a322d0422bf5963abdaf13f80766 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:30:00 +0000 Subject: [PATCH] refactor(session): make cmd_continue idempotent with ensure_* functions - Add ensure_worktree() - creates worktree if missing, returns status - Add ensure_session() - creates session if missing, handles inconsistent states - Add fork_agent() - extracted agent forking logic - Refactor cmd_continue() to use ensure_* functions (idempotent) - Make cmd_start() a thin wrapper calling cmd_continue() - Simplify daemon to always call cmd_continue (no existence check) This makes cmd_continue truly idempotent - it will: - Continue existing session if it exists - Create session and worktree if they don't exist - Clean and recreate if state is inconsistent Closes #168 --- .../kugetsu/scripts/kugetsu-queue-daemon.sh | 29 +-- skills/kugetsu/scripts/kugetsu-session.sh | 215 ++++++++++-------- 2 files changed, 128 insertions(+), 116 deletions(-) diff --git a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh index a36f1cb..cbfe507 100644 --- a/skills/kugetsu/scripts/kugetsu-queue-daemon.sh +++ b/skills/kugetsu/scripts/kugetsu-queue-daemon.sh @@ -109,28 +109,15 @@ process_task() { source "$SCRIPT_DIR/kugetsu-session.sh" - if worktree_exists "$issue_ref" "$WORKTREES_DIR" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then - log_file="$LOGS_DIR/delegate-$(date +%s).log" - if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then - sleep 1 - local session_id=$(get_session_id_for_issue "$issue_ref") - update_queue_item_state "$queue_id" "notified" "$session_id" "" - echo "Task $queue_id continued for $issue_ref" - else - update_queue_item_state "$queue_id" "error" - echo "Task $queue_id ($issue_ref) failed to continue" - fi + log_file="$LOGS_DIR/delegate-$(date +%s).log" + if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then + sleep 1 + local session_id=$(get_session_id_for_issue "$issue_ref") + update_queue_item_state "$queue_id" "notified" "$session_id" "" + echo "Task $queue_id continued for $issue_ref" else - log_file="$LOGS_DIR/delegate-$(date +%s).log" - if cmd_start "$issue_ref" "$message" >> "$log_file" 2>&1; then - sleep 1 - local session_id=$(get_session_id_for_issue "$issue_ref") - update_queue_item_state "$queue_id" "notified" "$session_id" "" - echo "Task $queue_id started for $issue_ref" - else - update_queue_item_state "$queue_id" "error" - echo "Task $queue_id ($issue_ref) failed to start" - fi + update_queue_item_state "$queue_id" "error" + echo "Task $queue_id ($issue_ref) failed to continue" fi release_lock "$issue_ref" diff --git a/skills/kugetsu/scripts/kugetsu-session.sh b/skills/kugetsu/scripts/kugetsu-session.sh index 2aeff34..521f596 100755 --- a/skills/kugetsu/scripts/kugetsu-session.sh +++ b/skills/kugetsu/scripts/kugetsu-session.sh @@ -330,29 +330,48 @@ EOF fi } -cmd_start() { - local issue_ref="${1:-}" - local message="${2:-}" +ensure_worktree() { + local issue_ref="$1" - if [ -z "$issue_ref" ]; then - echo "Error: issue ref is required" >&2 - echo "Usage: kugetsu start [message]" >&2 - exit 1 + if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then + log "info" "ensure_worktree" "Worktree already exists for $issue_ref" + echo "existed" + return 0 fi - validate_issue_ref "$issue_ref" - local base_session_id=$(get_base_session_id) if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then - echo "Error: Base session not found. Run 'kugetsu init' first." >&2 - exit 1 + log "error" "ensure_worktree" "Base session not found for $issue_ref" + echo "error" + return 1 fi + local active_count=$(count_active_dev_sessions) + if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then + log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref" + echo "error" + return 1 + fi + + if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then + log "info" "ensure_worktree" "Created worktree for $issue_ref" + echo "created" + return 0 + else + log "error" "ensure_worktree" "Failed to create worktree for $issue_ref" + echo "error" + return 1 + fi +} + +ensure_session() { + local issue_ref="$1" + local session_file=$(issue_ref_to_filename "$issue_ref") local session_path="$SESSIONS_DIR/$session_file" - local worktree_exists=false - if worktree_exists "$issue_ref"; then + local worktree_exists=false + if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then worktree_exists=true fi @@ -362,38 +381,46 @@ cmd_start() { fi if $worktree_exists && $session_exists; then - echo "Issue '$issue_ref' already has a worktree and session." >&2 - echo "Use 'kugetsu continue $issue_ref' to continue work." >&2 - exit 1 + log "info" "ensure_session" "Session already exists for $issue_ref" + echo "continued" + return 0 fi if $worktree_exists && ! $session_exists; then - echo "Warning: Worktree exists but session is missing. Removing worktree to recreate both..." >&2 + log "warn" "ensure_session" "Worktree exists but session is missing. Removing worktree to recreate both..." remove_worktree_for_issue "$issue_ref" worktree_exists=false fi if ! $worktree_exists && $session_exists; then - echo "Warning: Session exists but worktree is missing. Removing stale session to recreate both..." >&2 + log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..." rm -f "$session_path" remove_issue_from_index "$issue_ref" session_exists=false fi - local active_count=$(count_active_dev_sessions) - if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then - echo "Error: Max concurrent agents (${MAX_CONCURRENT_AGENTS:-3}) reached. Use 'kugetsu continue' or wait for an agent to finish." >&2 - exit 1 + if ! $worktree_exists; then + local wt_status=$(ensure_worktree "$issue_ref") + if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then + log "error" "ensure_session" "Failed to ensure worktree for $issue_ref" + echo "error" + return 1 + fi fi - create_worktree "$issue_ref" "$WORKTREES_DIR" + local base_session_id=$(get_base_session_id) + if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then + log "error" "ensure_session" "Base session not found for $issue_ref" + echo "error" + return 1 + fi local new_session_id=$(create_session "$base_session_id") if [ -z "$new_session_id" ]; then - echo "Error: Could not create session" >&2 - remove_worktree_for_issue "$issue_ref" - exit 1 + log "error" "ensure_session" "Could not create session for $issue_ref" + echo "error" + return 1 fi local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") @@ -403,89 +430,87 @@ cmd_start() { add_issue_to_index "$issue_ref" "$session_file" - local dev_message=$(build_dev_agent_message "$issue_ref" "$message") - - load_agent_env "dev" - - cd "$worktree_path" - local sanitized_id=$(echo "$new_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') - mkdir -p "$worktree_path/.kugetsu" - if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then - echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true - fi - local msg_file="$worktree_path/.kugetsu/msg.txt" - printf '%s' "$dev_message" > "$msg_file" - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 & - - echo "Session started for '$issue_ref': $new_session_id" - echo "Worktree: $worktree_path" + log "info" "ensure_session" "Created session for $issue_ref: $new_session_id" + echo "created" + return 0 } -cmd_continue() { - local session_name="" - local message="" - local args=("$@") - - args=$(set_debug_mode "${args[@]}") - - for arg in $args; do - if [ -z "$session_name" ]; then - session_name="$arg" - else - message="$arg" - fi - done +fork_agent() { + local session_id="$1" + local worktree_path="$2" + local message="$3" - if [ -z "$session_name" ]; then - echo "Error: issue ref is required" >&2 - echo "Usage: kugetsu continue [message]" >&2 - exit 1 - fi - - validate_issue_ref "$session_name" - - local session_file=$(get_session_for_issue "$session_name") - if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then - echo "Error: No session found for '$session_name'" >&2 - echo "Use 'kugetsu start $session_name' to create a new session." >&2 - exit 1 - fi - - local session_path="$SESSIONS_DIR/$session_file" - - if [ ! -f "$session_path" ]; then - echo "Error: Session file not found: $session_path" >&2 - exit 1 + if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then + log "error" "fork_agent" "Invalid worktree path: $worktree_path" + echo "error" + return 1 fi load_agent_env "dev" - local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "") - local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") - local issue_ref=$(python3 -c "import json; print(json.load(open('$session_path')).get('issue_ref', ''))" 2>/dev/null || echo "") - - if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then - echo "Warning: Worktree is missing for '$session_name'. Recovering..." >&2 - rm -f "$session_path" - remove_issue_from_index "$session_name" - echo "Calling cmd_start to create new session and worktree..." >&2 - cmd_start "$session_name" "$message" - return $? - fi - - if [ -z "$message" ]; then - message=$(build_dev_agent_message "$issue_ref" "") - fi - cd "$worktree_path" - local sanitized_id=$(echo "$opencode_session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') + local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') mkdir -p "$worktree_path/.kugetsu" if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true fi local msg_file="$worktree_path/.kugetsu/msg.txt" printf '%s' "$message" > "$msg_file" - nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 & + nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 & + + log "info" "fork_agent" "Forked agent for session $session_id in $worktree_path" + echo "forked" + return 0 +} + +cmd_start() { + cmd_continue "$@" +} + +cmd_continue() { + local issue_ref="${1:-}" + local message="${2:-}" + + if [ -z "$issue_ref" ]; then + echo "Error: issue ref is required" >&2 + echo "Usage: kugetsu continue [message]" >&2 + exit 1 + fi + + validate_issue_ref "$issue_ref" + + if [ -z "$message" ]; then + message=$(build_dev_agent_message "$issue_ref" "") + fi + + local worktree_status=$(ensure_worktree "$issue_ref") + if [ "$worktree_status" = "error" ]; then + echo "Error: Failed to ensure worktree for '$issue_ref'" >&2 + exit 1 + fi + + local session_status=$(ensure_session "$issue_ref") + if [ "$session_status" = "error" ]; then + echo "Error: Failed to ensure session for '$issue_ref'" >&2 + exit 1 + fi + + local session_file=$(issue_ref_to_filename "$issue_ref") + local session_path="$SESSIONS_DIR/$session_file" + local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "") + local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "") + + local fork_status=$(fork_agent "$opencode_session_id" "$worktree_path" "$message") + if [ "$fork_status" = "error" ]; then + echo "Error: Failed to fork agent for '$issue_ref'" >&2 + exit 1 + fi + + log "info" "cmd_continue" "Result for $issue_ref: worktree=$worktree_status session=$session_status fork=$fork_status" + + echo "Session continued for '$issue_ref': $opencode_session_id" + echo "Worktree: $worktree_path" + echo "${worktree_status}-${session_status}-${fork_status}" } cmd_list() {