diff --git a/scripts/opencode-builder.sh b/scripts/opencode-builder.sh old mode 100755 new mode 100644 index 8bac57096..daa400546 --- a/scripts/opencode-builder.sh +++ b/scripts/opencode-builder.sh @@ -19,6 +19,7 @@ BASE="http://${HOST}:${PORT}" AGENT="auto-agents" PROMPT="Complete the current project's milestones up to and including v3.7.0 to a production ready state" HEALTH_TIMEOUT=60 # seconds to wait for the server to become healthy +MAX_IDLE_PER_MINUTE=10 # ── Internal state ──────────────────────────────────────────────────────── SERVER_PID="" @@ -27,6 +28,8 @@ SESSION_ID="" STOP_LOOP=false LAST_SIGINT=0 OWN_SERVER=false +N=0 +IDLE_EVENTS_FILE="" # ── Helpers ─────────────────────────────────────────────────────────────── die() { printf "ERROR: %s\n" "$*" >&2; exit 1; } @@ -42,6 +45,9 @@ check_opencode_running() { } cleanup() { + if [[ -n "$IDLE_EVENTS_FILE" ]]; then + rm -f "$IDLE_EVENTS_FILE" 2>/dev/null || true + fi if $OWN_SERVER && [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then log "Stopping opencode server (PID $SERVER_PID)..." kill "$SERVER_PID" 2>/dev/null @@ -72,6 +78,83 @@ handle_sigint() { trap handle_sigint INT trap cleanup EXIT +# ── Idle-event tracking ─────────────────────────────────────────────────── +init_idle_tracking() { + IDLE_EVENTS_FILE="/tmp/opencode-builder-idle-events-${SESSION_ID}.txt" + : > "$IDLE_EVENTS_FILE" 2>/dev/null || true +} + +record_idle_event() { + local now + now=$(date +%s) + echo "$now" >> "$IDLE_EVENTS_FILE" +} + +count_idle_events_last_minute() { + local now cutoff + now=$(date +%s) + cutoff=$((now - 60)) + if [[ -f "$IDLE_EVENTS_FILE" ]]; then + awk -v c="$cutoff" '$1 > c {count++} END {print count+0}' "$IDLE_EVENTS_FILE" 2>/dev/null + else + echo 0 + fi +} + +# ── Session lifecycle ───────────────────────────────────────────────────── +create_session() { + log "Creating session..." + SESSION_ID=$( + curl -sf -X POST "${BASE}/session" \ + -H "Content-Type: application/json" \ + -d '{"title":"auto-agents"}' \ + | jq -r '.id' + ) || die "Failed to create session" + + [[ -n "$SESSION_ID" && "$SESSION_ID" != "null" ]] || die "Session ID is empty or null" + log "Session ID: ${SESSION_ID}" + init_idle_tracking +} + +delete_session() { + local sid="$1" + if [[ -n "$sid" ]]; then + curl -sf -X DELETE "${BASE}/session/${sid}" >/dev/null 2>&1 || true + fi +} + +restart_session() { + log "Session appears broken (>${MAX_IDLE_PER_MINUTE} idle events/min). Restarting..." + local old_sid="$SESSION_ID" + SESSION_ID="" + N=0 + delete_session "$old_sid" + create_session + send_message "$INIT_BODY" + log "Initial prompt complete after restart." +} + +# ── Question handling ───────────────────────────────────────────────────── +# Queries the OpenCode /question endpoint and rejects any pending questions +# that belong to the supplied session. Returns the number of questions that +# were successfully dismissed. +dismiss_pending_questions() { + local sid="$1" + local questions_json dismissed qid + + questions_json=$(curl -sf "${BASE}/question" 2>/dev/null) || return 1 + + dismissed=0 + for qid in $(echo "$questions_json" | jq -r --arg sid "$sid" '.[] | select(.sessionID == $sid) | .id'); do + log "Dismissing question $qid" + if curl -sf -X POST "${BASE}/question/${qid}/reject" >/dev/null 2>&1; then + dismissed=$((dismissed + 1)) + fi + done + + echo "$dismissed" +} + # ── Preflight checks ───────────────────────────────────────────────────── for cmd in curl jq opencode; do command -v "$cmd" &>/dev/null || die "'$cmd' is required but not found in PATH" @@ -94,7 +177,7 @@ else opencode serve --port "$PORT" --hostname "$HOST" & SERVER_PID=$! OWN_SERVER=true - + log "Waiting for server to become healthy (up to ${HEALTH_TIMEOUT}s)..." healthy=false for (( i = 1; i <= HEALTH_TIMEOUT; i++ )); do @@ -110,16 +193,7 @@ else fi # ── Create a session ────────────────────────────────────────────────────── -log "Creating session..." -SESSION_ID=$( - curl -sf -X POST "${BASE}/session" \ - -H "Content-Type: application/json" \ - -d '{"title":"auto-agents"}' \ - | jq -r '.id' -) || die "Failed to create session" - -[[ -n "$SESSION_ID" && "$SESSION_ID" != "null" ]] || die "Session ID is empty or null" -log "Session ID: ${SESSION_ID}" +create_session # ── send_message ────────────────────────────────────────────────────────── # Sends a JSON body to POST /session/:id/message and blocks until the LLM @@ -158,13 +232,34 @@ log "Initial prompt complete." # ── Continue loop ───────────────────────────────────────────────────────── CONT_BODY=$(jq -nc --arg a "$AGENT" '{agent: $a, parts: [{type: "text", text: "continue with your mainloop, monitor the supervisors, keep them alive, and sleep in an infinite loop."}]}') -N=0 while ! $STOP_LOOP; do N=$((N + 1)) - log "── continue #${N} ──" - send_message "$CONT_BODY" - log "continue #${N} complete." + + # Check idle-event rate in the last minute + idle_count=$(count_idle_events_last_minute) + if (( idle_count > MAX_IDLE_PER_MINUTE )); then + restart_session + continue + fi + + log "── iteration #${N} ──" + + # Inspect messages: if a question is waiting for this session, dismiss it; + # otherwise issue the standard continue command. + dismissed=0 + if dismissed_raw=$(dismiss_pending_questions "$SESSION_ID"); then + dismissed="$dismissed_raw" + fi + + if (( dismissed > 0 )); then + log "Dismissed ${dismissed} pending question(s)." + record_idle_event + else + send_message "$CONT_BODY" + log "continue #${N} complete." + record_idle_event + fi done echo ""