diff --git a/.env.example b/.env.example index 6d127382479..698ce2e3f95 100644 --- a/.env.example +++ b/.env.example @@ -7,12 +7,23 @@ # All defaults here work with `docker compose up` out of the box. # # Service ports (defaults): -# Postgres → localhost:5432 -# Redis → localhost:6379 +# Postgres → localhost:5432 (override with PGPORT + DATABASE_URL together) +# Redis → localhost:6379 (override with BUZZ_REDIS_HOST_PORT + REDIS_URL) # Typesense → localhost:8108 -# Adminer → localhost:8082 (DB browser UI) +# Adminer → localhost:8082 (override with BUZZ_ADMINER_HOST_PORT) +# MinIO/S3 → localhost:9000 (override with BUZZ_MINIO_HOST_PORT + BUZZ_S3_ENDPOINT) +# MinIO cons. → localhost:9001 (override with BUZZ_MINIO_CONSOLE_HOST_PORT) # -# Note: If port 8082 conflicts, change the adminer port in docker-compose.yml +# If Homebrew (or another) service already owns one of these host ports, pick a +# free host port and keep the paired connection URL in sync, for example: +# PGPORT=55432 +# DATABASE_URL=postgres://buzz:buzz_dev@localhost:55432/buzz +# BUZZ_REDIS_HOST_PORT=56379 +# REDIS_URL=redis://localhost:56379 +# BUZZ_MINIO_HOST_PORT=59000 +# BUZZ_S3_ENDPOINT=http://localhost:59000 +# BUZZ_MINIO_CONSOLE_HOST_PORT=59001 +# BUZZ_ADMINER_HOST_PORT=58082 # ============================================================================= # ----------------------------------------------------------------------------- @@ -30,6 +41,8 @@ PGDATABASE=buzz # ----------------------------------------------------------------------------- # Redis 7 # ----------------------------------------------------------------------------- +# Host port published by docker-compose (container still listens on 6379). +# BUZZ_REDIS_HOST_PORT=6379 REDIS_URL=redis://localhost:6379 # Max connections in the relay's shared Redis pool (default 16). # BUZZ_REDIS_POOL_SIZE=16 @@ -151,6 +164,16 @@ BUZZ_PUSH_ENABLED=false # BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS=2 # ----------------------------------------------------------------------------- +# Host ports published by docker-compose (container still listens on service ports). +# If a local Postgres/Redis/MinIO owns the defaults, pick free host ports and keep +# matching client env vars in sync (#2479). +# PGPORT=5432 +# BUZZ_REDIS_HOST_PORT=6379 +# BUZZ_ADMINER_HOST_PORT=8082 +# BUZZ_MINIO_HOST_PORT=9000 +# BUZZ_MINIO_CONSOLE_HOST_PORT=9001 + + # S3-Compatible Object Storage (media + Git/CAS) # ----------------------------------------------------------------------------- # The local MinIO container is reachable from host processes at localhost:9000. @@ -163,6 +186,7 @@ BUZZ_S3_BUCKET=buzz-media BUZZ_S3_REGION=us-east-1 BUZZ_S3_ADDRESSING_STYLE=path + # ----------------------------------------------------------------------------- # Media Upload Admission # ----------------------------------------------------------------------------- diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index d81602e2019..6bc3c6fa308 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -525,7 +525,16 @@ async fn main() -> anyhow::Result<()> { .git_store .run_conformance_probe(cfg) .await - .map_err(|e| anyhow::anyhow!("git conformance probe failed: {e}"))?; + .map_err(|e| { + anyhow::anyhow!( + "git conformance probe failed: {e}. \ + Object stores that do not honor S3 If-Match / If-None-Match \ + (notably GCS via the S3-interop XML API) cannot satisfy the \ + A3 pointer-CAS gate. Use a CAS-capable backend (MinIO, AWS S3), \ + or set BUZZ_GIT_CONFORMANCE_PROBE=false to skip admission \ + (git ref updates will not be linearizable)." + ) + })?; tracing::info!( race_width = report.race_width, race_rounds = report.race_rounds, diff --git a/deploy/charts/buzz/templates/deployment.yaml b/deploy/charts/buzz/templates/deployment.yaml index 319ec7f1594..bf534d5de10 100644 --- a/deploy/charts/buzz/templates/deployment.yaml +++ b/deploy/charts/buzz/templates/deployment.yaml @@ -163,6 +163,7 @@ spec: - { name: BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS, value: {{ .Values.git.packCacheMaxConcurrentPopulations | quote }} } - { name: BUZZ_GIT_MAX_REPOS_PER_PUBKEY, value: {{ .Values.git.maxReposPerPubkey | quote }} } - { name: BUZZ_GIT_MAX_CONCURRENT_OPS, value: {{ .Values.git.maxConcurrentOps | quote }} } + - { name: BUZZ_GIT_CONFORMANCE_PROBE, value: {{ .Values.git.conformanceProbe | quote }} } # ── S3 (non-secret) ────────────────────────────────────── {{- $s3Endpoint := include "buzz.s3Endpoint" . }} diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml index 6c57a5c8ac9..342d0d86e7f 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -382,6 +382,10 @@ git: packCacheVolumeSize: 7Gi # per-pod emptyDir; includes cold-population staging maxReposPerPubkey: 100 maxConcurrentOps: 20 + # Startup A3 probe (S3 conditional-write CAS). Default true. Set false only + # for backends that cannot honor If-Match (e.g. GCS S3-interop) — see #2470. + # Skipping means git pointer updates are not linearizable. + conformanceProbe: true # ── Migrations ─────────────────────────────────────────────────────────────── # Relay runs sqlx migrations at startup via BUZZ_AUTO_MIGRATE=true. diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index 135224d01db..abdbc35bbff 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -162,16 +162,21 @@ pub async fn list_managed_agent_runtimes( .managed_agent_processes .lock() .map_err(|e| e.to_string())?; - let exited_keys: Vec<_> = runtimes + // Reap exited children the same way as `sync_managed_agent_processes` so + // unexpected harness deaths set `last_error` / `last_exit_code` (and the + // Agents UI can show them) instead of silently clearing the runtime + // (#2453). Capture wait results before removing map entries. + let exited: Vec<_> = runtimes .iter_mut() .filter_map(|(key, runtime)| match runtime.child.try_wait() { - Ok(Some(_)) | Err(_) => Some(key.clone()), + Ok(Some(status)) => Some((key.clone(), Ok(status), runtime.log_path.clone())), + Err(error) => Some((key.clone(), Err(error), runtime.log_path.clone())), Ok(None) => None, }) .collect(); - let records_changed = !exited_keys.is_empty(); + let records_changed = !exited.is_empty(); let mut statuses = Vec::new(); - for key in exited_keys { + for (key, wait_result, log_path) in exited { runtimes.remove(&key); super::remove_agent_runtime_receipt(&app, &key); state.clear_agent_session_cache(&key); @@ -181,6 +186,29 @@ pub async fn list_managed_agent_runtimes( { record.updated_at = crate::util::now_iso(); record.last_stopped_at = Some(record.updated_at.clone()); + match wait_result { + Ok(status) => { + record.last_exit_code = status.code(); + if status.success() { + record.last_error = + Some("agent runtime exited unexpectedly".to_string()); + record.last_error_code = None; + } else { + let log_err = super::meaningful_agent_error_from_log(&log_path) + .unwrap_or_else(|| super::storage::AgentLogError { + message: format!("harness exited with status {status}"), + code: None, + }); + record.last_error = Some(log_err.message); + record.last_error_code = log_err.code; + } + } + Err(error) => { + record.last_error = + Some(format!("failed to inspect process state: {error}")); + record.last_error_code = None; + } + } let status = status_for_with( &app, record, @@ -196,6 +224,9 @@ pub async fn list_managed_agent_runtimes( statuses.push(status); } } + if records_changed { + let _ = app.emit("agents-data-changed", ()); + } statuses.extend(runtimes.iter().filter_map(|(key, runtime)| { let record = records .iter() diff --git a/docker-compose.yml b/docker-compose.yml index e7dc09fafc4..c7c61c649c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: POSTGRES_DB: buzz PGDATA: /var/lib/postgresql/data ports: - - "127.0.0.1:5432:5432" + # Host port follows PGPORT so local Homebrew Postgres can keep :5432 (#2479). + - "127.0.0.1:${PGPORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data networks: @@ -34,7 +35,8 @@ services: image: redis:7-alpine container_name: buzz-redis ports: - - "127.0.0.1:6379:6379" + # Host port follows BUZZ_REDIS_HOST_PORT so local Redis can keep :6379 (#2479). + - "127.0.0.1:${BUZZ_REDIS_HOST_PORT:-6379}:6379" networks: - buzz-net healthcheck: @@ -56,7 +58,8 @@ services: image: adminer:latest container_name: buzz-adminer ports: - - "127.0.0.1:8082:8080" + # Host port follows BUZZ_ADMINER_HOST_PORT so a local :8082 can be freed (#2479). + - "127.0.0.1:${BUZZ_ADMINER_HOST_PORT:-8082}:8080" networks: - buzz-net depends_on: @@ -108,8 +111,10 @@ services: MINIO_ROOT_USER: buzz_dev MINIO_ROOT_PASSWORD: buzz_dev_secret ports: - - "127.0.0.1:9000:9000" - - "127.0.0.1:9001:9001" + # Host ports follow BUZZ_MINIO_HOST_PORT / BUZZ_MINIO_CONSOLE_HOST_PORT so a + # local MinIO/S3 can keep :9000/:9001. Keep BUZZ_S3_ENDPOINT in sync (#2479). + - "127.0.0.1:${BUZZ_MINIO_HOST_PORT:-9000}:9000" + - "127.0.0.1:${BUZZ_MINIO_CONSOLE_HOST_PORT:-9001}:9001" volumes: - minio-data:/data networks: diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh index ae358517b34..a269364e599 100755 --- a/scripts/dev-setup.sh +++ b/scripts/dev-setup.sh @@ -89,11 +89,13 @@ fail_if_local_redis_blocks_compose() { if docker ps --format '{{.Names}}' | grep -qx 'buzz-redis'; then return fi + # Match the compose host publish port (see docker-compose.yml / #2479). + local redis_host_port="${BUZZ_REDIS_HOST_PORT:-6379}" local redis_pids - redis_pids=$(lsof -nP -iTCP:6379 -sTCP:LISTEN 2>/dev/null | awk 'NR > 1 && $1 == "redis-ser" {print $2}' | sort -u | tr ' + redis_pids=$(lsof -nP -iTCP:"${redis_host_port}" -sTCP:LISTEN 2>/dev/null | awk 'NR > 1 && $1 == "redis-ser" {print $2}' | sort -u | tr ' ' ' ' || true) if [[ -n "${redis_pids}" ]]; then - error "Local Redis is already listening on port 6379 (pid(s): ${redis_pids}). Stop it before running setup: brew services stop redis" + error "Local Redis is already listening on port ${redis_host_port} (pid(s): ${redis_pids}). Stop it, or set BUZZ_REDIS_HOST_PORT / REDIS_URL to a free host port before running setup." exit 1 fi } @@ -187,7 +189,7 @@ echo -e "${GREEN}=======================================================${NC}" echo "" echo -e " ${BLUE}Postgres${NC} ${DATABASE_URL}" echo -e " ${BLUE}Redis${NC} ${REDIS_URL}" -echo -e " ${BLUE}Adminer${NC} http://localhost:8082 (DB browser)" +echo -e " ${BLUE}Adminer${NC} http://localhost:${BUZZ_ADMINER_HOST_PORT:-8082} (DB browser)" echo -e " ${BLUE}Keycloak${NC} http://localhost:8180 (admin / admin — local OAuth testing)" echo "" echo -e " ${YELLOW}Next steps:${NC}"