sipeed/picoclaw · error

Failed to stop gateway (PID %d): %v

Error message

Failed to stop gateway (PID %d): %v

What it means

Returned by POST /api/gateway/stop when stopGatewayLocked() fails (gateway.go:942-974). Two causes: the safety guard 'refuse to stop non-gateway process (PID %d)' - the tracked PID was recycled to a non-picoclaw process (gatewayProcessMatcher inspected it and it is not a gateway), so the backend deliberately refuses to signal it - or the SIGTERM (SIGKILL on Windows) itself failed, e.g. 'os: process already finished' for a process that exited between the liveness check and the signal, or permission denied.

Source

Thrown at web/backend/api/gateway.go:1279

// Note: Unlike StopGateway (which only stops self-started processes), this API endpoint
// stops any gateway process, including attached ones. This is intentional for user control.
//
//	POST /api/gateway/stop
func (h *Handler) handleGatewayStop(w http.ResponseWriter, r *http.Request) {
	gateway.mu.Lock()
	defer gateway.mu.Unlock()

	if gateway.cmd == nil || gateway.cmd.Process == nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{
			"status": "not_running",
		})
		return
	}

	pid, err := stopGatewayLocked()
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to stop gateway (PID %d): %v", pid, err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"status": "ok",
		"pid":    pid,
	})
}

// RestartGateway restarts the gateway process. This is a non-blocking operation
// that stops the current gateway (if running) and starts a new one.
// Returns the PID of the new gateway process or an error.
func (h *Handler) RestartGateway() (int, error) {
	ready, reason, err := h.gatewayStartReady()
	if err != nil {
		return 0, fmt.Errorf("failed to validate gateway start conditions: %w", err)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Call GET /api/gateway/status - if the gateway is not running, there is nothing to stop
  2. If the refuse-guard tripped, identify the PID with ps before touching it - it belongs to another program; do NOT kill it blindly
  3. Use POST /api/gateway/restart instead, which re-validates and resets tracked state, or restart the web backend to clear stale tracking
  4. For 'process already finished' races, simply re-check status - the stop effectively succeeded
Defensive patterns

Strategy: validation

Validate before calling

// Only attempt a stop when the API itself reports a running gateway.
const st = await (await fetch('/api/gateway/status')).json();
if (st.gateway_status !== 'running') {
  return {skipped: true};   // nothing to stop - avoids the not_running/race paths
}

Try / catch

const res = await fetch('/api/gateway/stop', {method: 'POST'});
const body = await res.json().catch(() => null);
if (res.ok || body?.status === 'not_running') return;      // success flavors
if (res.status === 500) {
  const text = await res.text();
  if (text.includes('refuse to stop non-gateway process')) {
    // safety guard: PID was recycled - do NOT retry, do NOT kill manually
    throw new Error('Tracked PID belongs to another process; restart the launcher to reset state');
  }
  if (text.includes('already finished')) return;           // race: it exited anyway
  throw new Error(text);
}

Prevention

When it happens

Trigger: Gateway died, OS recycled its PID to an unrelated process, and the API still tracks the old PID; double-stop race where the process exits right after the guard check; signaling a zombie child that cannot receive SIGTERM.

Common situations: Long-running hosts where PID reuse is likely; external kill -9 of the gateway leaving stale tracked state; rapid stop/status/stop sequences from automation.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/90894ed18cde91db. Report an issue: GitHub.