From 9ffeaa00cc2a6aedad514692790fdf222f3b78c1 Mon Sep 17 00:00:00 2001 From: Taksh Date: Thu, 23 Jul 2026 15:35:24 +0300 Subject: [PATCH 1/3] fix(desktop): surface unexpected agent runtime exits Record last_error when the runtime poller reaps a dead harness so Agents no longer look merely idle after a silent crash. Signed-off-by: Taksh Co-authored-by: Cursor --- .../src/managed_agents/runtime_commands.rs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index c0e55184b..e7f7c6e4f 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -160,16 +160,21 @@ pub 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); @@ -179,6 +184,28 @@ pub 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() { + // Stopped without our stop command — still a signal. + 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, @@ -194,6 +221,9 @@ pub 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() @@ -212,7 +242,8 @@ pub fn list_managed_agent_runtimes( })); drop(runtimes); // Records are only mutated above when a runtime exited — skip the store - // rewrite on the common nothing-changed poll. + // rewrite on the common nothing-changed poll. (`agents-data-changed` is + // emitted earlier so the Agents list refreshes alongside status events.) if records_changed { save_managed_agents(&app, &records)?; } From 909ad092455632bcce7f479df61f84266afcb5ca Mon Sep 17 00:00:00 2001 From: Taksh Date: Thu, 23 Jul 2026 15:35:25 +0300 Subject: [PATCH 2/3] fix(dev): allow custom Postgres and Redis host ports Publish compose ports from PGPORT / BUZZ_REDIS_HOST_PORT so setup works alongside Homebrew services on :5432/:6379. Signed-off-by: Taksh Co-authored-by: Cursor --- .env.example | 13 +++++++++++-- docker-compose.yml | 6 ++++-- scripts/dev-setup.sh | 6 ++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 1e69c6a0e..569dc627f 100644 --- a/.env.example +++ b/.env.example @@ -7,11 +7,18 @@ # 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) # +# If Homebrew (or another) Postgres/Redis already owns :5432/:6379, pick free +# host ports and keep the connection URLs 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 +# # Note: If port 8082 conflicts, change the adminer port in docker-compose.yml # ============================================================================= @@ -30,6 +37,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 # ----------------------------------------------------------------------------- diff --git a/docker-compose.yml b/docker-compose.yml index 0056ea6a4..27d3bbd1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: POSTGRES_DB: buzz PGDATA: /var/lib/postgresql/data ports: - - "5432:5432" + # Host port follows PGPORT so local Homebrew Postgres can keep :5432 (#2479). + - "${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: - - "6379:6379" + # Host port follows BUZZ_REDIS_HOST_PORT so local Redis can keep :6379 (#2479). + - "${BUZZ_REDIS_HOST_PORT:-6379}:6379" networks: - buzz-net healthcheck: diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh index ae358517b..3a35470a7 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 } From c70affbdd8959e1256b2b9bf437e833a2302d656 Mon Sep 17 00:00:00 2001 From: Taksh Date: Thu, 23 Jul 2026 15:35:25 +0300 Subject: [PATCH 3/3] fix(relay): explain A3 probe failures for non-CAS stores Point operators at BUZZ_GIT_CONFORMANCE_PROBE and expose the flag in the Helm chart (GCS S3-interop cannot pass If-Match). Signed-off-by: Taksh Co-authored-by: Cursor --- crates/buzz-relay/src/main.rs | 11 ++++++++++- deploy/charts/buzz/templates/deployment.yaml | 1 + deploy/charts/buzz/values.yaml | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index 00ef7819c..238e79bd2 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -490,7 +490,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 f8d67de31..fe6c64b79 100644 --- a/deploy/charts/buzz/templates/deployment.yaml +++ b/deploy/charts/buzz/templates/deployment.yaml @@ -150,6 +150,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 180afc674..b271594c5 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -343,6 +343,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.