diff --git a/scripts/opencode-builder.sh b/scripts/opencode-builder.sh index 11996996a..a9b9f27cf 100755 --- a/scripts/opencode-builder.sh +++ b/scripts/opencode-builder.sh @@ -9,12 +9,20 @@ set -uo pipefail +# ── Debug mode ───────────────────────────────────────────────────────────── +if [[ "${BUILDER_DEBUG:-0}" == "1" ]]; then + set -x + log "DEBUG MODE ENABLED" +fi + # ── Configuration (override via environment) ────────────────────────────── PORT="${OPENCODE_PORT:-4096}" HOST="${OPENCODE_HOST:-127.0.0.1}" BASE="http://${HOST}:${PORT}" HEALTH_TIMEOUT=60 MAX_IDLE_PER_MINUTE=10 + SCRIPT_TIMEOUT_SEC=300 # timeout for list_prs / list_issues scripts (Forgejo is slow — allow 5 min) +SESSION_SCRIPT_TIMEOUT=30 # timeout for session_* scripts # Worker pool sizing total_max_workers="${CA_MAX_PARALLEL_WORKERS:-4}" @@ -129,7 +137,7 @@ query_all_sessions() { find_sessions_by_prefix() { local prefix="$1" local status_filter="${2:-all}" - $SESSION_FIND_PREFIX --server "${BASE}" --prefix "$prefix" --exclude-supervisor --status "$status_filter" 2>/dev/null + timeout "$SESSION_SCRIPT_TIMEOUT" $SESSION_FIND_PREFIX --server "${BASE}" --prefix "$prefix" --exclude-supervisor --status "$status_filter" 2>/dev/null } get_busy_worker_tags() { @@ -278,54 +286,68 @@ fetch_list_script() { fi local result - result=$(npx --yes tsx "$script_path" \ + result=$(timeout "$SCRIPT_TIMEOUT_SEC" npx --yes tsx "$script_path" \ --url "$forgejo_url" \ --pat "$forgejo_pat" \ --owner "$forgejo_owner" \ --repo "$forgejo_repo" 2>/dev/null) + local status=$? + if [[ $status -eq 139 || $status -eq 137 || $status -eq 143 || $status -eq 124 ]]; then + log "[fetch_list_script] WARNING: $(basename "$script_path") timed out after ${SCRIPT_TIMEOUT_SEC}s (exit ${status})" + echo "[]" + return + fi + if [[ $status -ne 0 ]]; then + log "[fetch_list_script] WARNING: $(basename "$script_path") failed with exit ${status}" + echo "[]" + return + fi local first_char="${result%"${result#?}"}" if [[ "$first_char" == "[" ]]; then echo "$result" else + log "[fetch_list_script] WARNING: $(basename "$script_path") produced non-JSON output (starts with '${first_char}'); treating as empty" echo "[]" fi } # ── Worker prompt builders ───────────────────────────────────────────────── +# All fields (title, body) have newlines collapsed so the prompt file stays +# valid as a single-line-ish text file. The full item JSON is embedded compact +# (jq -c . → one line, no literal newlines) at the end of the prompt. +# session_start.ts reads this file as plain text and sends it to the agent. build_impl_prompt() { local item_json="$1" local kind="$2" local work_num="$3" - local title body milestone milestone_id milestone_title labels_json - title=$(echo "$item_json" | jq -r '.title') - body=$(echo "$item_json" | jq -r '.body // ""') - if [[ "$kind" == "ISSUE" ]]; then - milestone=$(echo "$item_json" | jq -r '.milestone') - else - milestone=$(echo "$item_json" | jq -r '.milestone // null') - fi - milestone_id=$(echo "$milestone" | jq -r 'if . == null then 0 else .id end') - milestone_title=$(echo "$milestone" | jq -r 'if . == null then "" else .title end') - labels_json=$(echo "$item_json" | jq -r '.labels') + local work_type; work_type=$([ "$kind" == "ISSUE" ] && echo "issue_impl" || echo "pr_fix") + local title body milestone_id milestone_title labels_json compact_item + title=$(echo "$item_json" | jq -r '.title' | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g') + body=$(echo "$item_json" | jq -r '.body // ""' | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g') + milestone_id=$(echo "$item_json" | jq -r '.milestone.id // 0') + milestone_title=$(echo "$item_json" | jq -r '.milestone.title // ""' | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g') + labels_json=$(echo "$item_json" | jq -c '.labels // []') + compact_item=$(echo "$item_json" | jq -c .) cat < 0 then "approved" else "not_approved" end') + compact_item=$(echo "$pr_json" | jq -c .) cat < "$out_file" 2>/dev/null & + cache_pids+=($!) + done + done + + log "[pool:${prefix}] waiting for ${#cache_pids[@]} pre-fetch jobs to complete..." + local cache_idx=0 + for pid in "${cache_pids[@]}"; do + wait "$pid" 2>/dev/null + local ck="${cache_keys[$cache_idx]}" + local out_file="${cache_dir}/${ck}.json" + if [[ -s "$out_file" ]]; then + local first_char + first_char=$(head -c1 "$out_file" 2>/dev/null) + if [[ "$first_char" == "[" ]]; then + log "[pool:${prefix}] ${ck}: ready ($(wc -l < "$out_file") lines)" + else + log "[pool:${prefix}] ${ck}: invalid output (starts with '${first_char}'), treating as empty" + echo "[]" > "$out_file" + fi + else + log "[pool:${prefix}] ${ck}: empty or timeout, treating as empty" + echo "[]" > "$out_file" + fi + ((cache_idx++)) + done + log "[pool:${prefix}] pre-fetch complete, starting worker selection..." + + # ── Worker selection: iterate cached work-groups ───────────────────── local launched=0 for type_spec in "${work_groups[@]}"; do if (( launched >= available_slots )); then @@ -783,7 +863,7 @@ main_loop() { fi IFS='|' read -r wg_label wg_scripts wg_kind <<< "$type_spec" - log "[${prefix}] checking work-group: '${wg_label}'" + log "[pool:${prefix}] work-group '${wg_label}': selecting workers..." local scripts_arr IFS=',' read -ra scripts_arr <<< "$wg_scripts" @@ -793,14 +873,17 @@ main_loop() { fi [[ -n "$script_path" ]] || continue + local cache_key="${script_path##*/}" + cache_key="${cache_key%.ts}" + local out_file="${cache_dir}/${cache_key}.json" local json - json=$(fetch_list_script "$script_path" "$FORGEJO_URL" "$FORGEJO_OWNER" "$FORGEJO_REPO" "$FORGEJO_PAT") + json=$(cat "$out_file" 2>/dev/null || echo "[]") local raw_count raw_count=$(echo "$json" | jq 'length' 2>/dev/null || echo 0) - log "[${prefix}] script $(basename "$script_path"): ${raw_count} items fetched" + log "[pool:${prefix}] script $(basename "$script_path"): ${raw_count} items in cache" - if [[ "$raw_count" -eq 0 || "$raw_count" == "null" ]]; then + if [[ "$raw_count" -eq 0 || "$raw_count" == "null" || "$raw_count" == "" ]]; then continue fi @@ -840,7 +923,7 @@ main_loop() { batch_count=$((available_slots - launched)) fi - log "[${prefix}] '${wg_label}': launching ${batch_count} workers in parallel (${#items_to_launch[@]} items in group)" + log "[pool:${prefix}] '${wg_label}': launching ${batch_count} workers in parallel (${#items_to_launch[@]} candidates)" local -a pids=() local -a tmp_files=() @@ -887,6 +970,7 @@ main_loop() { done log "Pool ${prefix}: done — ${launched} workers launched this iteration" + rm -rf "$cache_dir" done fi