Deep Review: 20260417-165817-pr-323

Date2026-04-17 16:58
Reporancher-sandbox/rancher-desktop-daemon
Round1 (of PR #323)
Author@mook-as
PR#323 — implement log passthrough
Commitsf1e7da8 implement log passthrough
ReviewersClaude Opus 4.7 (effort: xhigh), Codex GPT 5.4 (effort: high), Gemini 3.1 Pro (effort: default)
VerdictRework — handler panics when the Docker watcher is absent (any non-moby or non-running state) and buffers the whole log session into a single WebSocket message, so live tailing does not work and the new endpoint crashes the daemon under normal lifecycle transitions.
Wall-clock time30 min 23 s


Executive Summary

PR #323 adds a PassthroughController implementation on the engine controller that streams Docker container logs over a WebSocket at /passthrough/engine/logs/{id}. The structural scaffolding — GetPassthroughEndpoints, GetPassthroughHandler, the controller.go wiring — matches the existing mock controller, but the runtime behaviour in HandleLogs has two independent blockers: it dereferences r.watcher without taking watcherMu or checking for nil, so any request outside a steady "running on moby" window panics the daemon; and it opens a single NextWriter(TextMessage) and copies the entire Follow stream into that one message, so clients see nothing until the container exits. The underlying io.ReadCloser is also never closed, so every request leaks a Docker HTTP connection after the handler returns. Error-path logging is pinned at V(5), which silences all diagnostics at the default verbosity. The PR is scoped to Docker only ("TODO: Figure out containerd") and ships no tests for the new endpoint.


Critical Issues

C1. HandleLogs nil-dereferences and races on r.watcher Claude Codex Gemini
		http.Error(w, "Invalid container ID", http.StatusBadRequest)
		return
	}

	// TODO: Figure out containerd
	reader, err := r.watcher.cli.ContainerLogs(
		req.Context(),
		containerID,
		client.ContainerLogsOptions{
			ShowStdout: true,
			ShowStderr: true,

EngineReconciler documents watcherMu explicitly for external readers (engine_reconciler.go:78-81), and every other access to r.watcherstartWatcherAndSync, stopWatcher, the dead-watcher sweep in Reconcile — takes the lock. HandleLogs reads r.watcher without the lock and without a nil check, so two independent problems follow.

	// the lock has no other role.
	watcherMu sync.Mutex
	watcher   *dockerWatcher

	// watcherCtx is the parent context for every Docker watcher the
	// reconciler starts. A manager.RunnableFunc cancels it on
	// shutdown, so the watcher outlives individual Reconcile calls
	// but not the manager. Deriving from Reconcile's ctx would leak
	// the Docker client: once the manager context cancels, Reconcile
	// no longer runs, and stopWatcher is unreachable from that path.
	watcherCtx    context.Context
	watcherCancel context.CancelFunc
}

First, the nil dereference. r.watcher is nil at startup, whenever App.status.Running != True, whenever App.spec.containerEngine.name != "moby" (the reconciler's own wantWatcher = running && engineIsDocker gate at engine_reconciler.go:136), and after any Docker disconnect (line 121 clears it). Discovery advertises the passthrough endpoint unconditionally, and the guard in GetPassthroughHandler only covers the window before c.reconciler is installed. A request that arrives in any of those normal states panics the HTTP goroutine. http.Server recovers the panic for that one request, but the same panic path is reachable from RDD's own UI and from local CLI callers — a trivial crash trigger for any endpoint consumer.

		if watcherRunning {
			log.Info("Stopping Docker watcher",
				"running", running, "engine", app.Spec.ContainerEngine.Name)
			r.stopWatcher()
		}
		terminalReason := "Stopped"
		terminalStatus := metav1.ConditionFalse
		terminalMessage := "Container engine stopped"
		if running && !engineIsDocker {
			// Report NotApplicable as Status=True so `rdd set
			// running=true containerEngine.name=containerd` stops

Second, the data race. Even when r.watcher is non-nil at the moment of the read, HandleLogs races with Reconcile/stopWatcher writing r.watcher. go test -race will flag this immediately. Under the Go memory model the read is undefined behaviour: the handler may observe a torn pointer, or use a watcher whose cli was already closed by run's deferred w.cli.Close() (docker_watcher.go:158).

	//
	// The order matters: if enqueueReconcile ran before w.done closed,
	// the reconciler could wake, see alive()==true on the about-to-exit
	// goroutine, and skip the restart. Closing cli between done and
	// enqueue means the reconciler observes the dead watcher only
	// after its client has been released.
	defer w.enqueueReconcile()
	defer w.cli.Close()
	defer close(w.done)
	// Keep a bad event shape from crashing the whole app-controller.
	defer func() {

Fix: snapshot the watcher under the mutex and return 503 when it is absent.

r.watcherMu.Lock()
watcher := r.watcher
r.watcherMu.Unlock()
if watcher == nil {
    http.Error(w, "Container engine not ready", http.StatusServiceUnavailable)
    return
}
reader, err := watcher.cli.ContainerLogs(...)

A withClient(func(*client.Client) error) helper on EngineReconciler would lock, nil-check, and invoke the closure in one place — keeping the invariant enforced where the rest of the file already enforces it, and preventing the next passthrough endpoint (exec, stats, non-follow logs) from re-introducing the same bug.

C2. Single WebSocket message prevents live log streaming Codex Gemini
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

	writer, err := conn.NextWriter(websocket.TextMessage)
	if err != nil {
		log.V(5).Info("Failed to get next writer", "error", err)
		return
	}
	defer writer.Close()

	_, err = stdcopy.StdCopy(writer, writer, reader)
	if err != nil {
		log.V(5).Info("Failed to copy container logs", "error", err)
		// At this point we already sent the data, so we can't send HTTP status.
	}
}

conn.NextWriter returns an io.WriteCloser that represents a single WebSocket message. StdCopy loops over the Follow: true Docker log stream, writing to that one writer for the entire session. Gorilla's internal frame buffer flushes when it fills (default 4 KiB), but the message's FIN bit is not set until writer.Close() runs — which does not happen until StdCopy returns, i.e. until the container exits. A strict WebSocket client (browser WebSocket API, anything that reassembles a complete message before delivery) will see nothing until then; a low-volume log trickles out in partial-frame bursts of up to 4 KiB, none of them terminated.

This also breaks the contract documented in docs/design/api_containers.md:217-218 ("messages are one way, text only, with each line being one message") and diverges from the mock at pkg/controllers/mock/logs_passthrough.go:65-67, which sends one WriteMessage per line — so the mock-backed BATS coverage passes while the real implementation does not stream.

    status: False
```

If `.spec.namespace` / `.spec.name` duplicates an existing container, a
`CreateFailed` status is set with some details.

An admission controller will ensure that we cannot have multiple
`ContainerRequest` objects at the same time for the same containerd
`name`/`namespace` pair.

#### Change container state
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

	// Write a message per line to simulate the real logs better.
	write := func(line string) error {
		err := conn.WriteMessage(websocket.TextMessage, []byte(line))
		if err != nil {
			log.V(5).Info("Failed to write WebSocket message", "error", err)
		}
		return err
	}

A secondary problem compounds it: TextMessage requires valid UTF-8, but Docker container stdout is routinely non-UTF-8 (binary output, ls --color=always, NUL bytes, non-UTF-8 locales). A strict client errors on the first non-UTF-8 payload.

Fix: emit each chunk as a discrete BinaryMessage instead of reusing one NextWriter.

type wsWriter struct{ conn *websocket.Conn }

func (w *wsWriter) Write(p []byte) (int, error) {
    if err := w.conn.WriteMessage(websocket.BinaryMessage, p); err != nil {
        return 0, err
    }
    return len(p), nil
}

_, err = stdcopy.StdCopy(&wsWriter{conn}, &wsWriter{conn}, reader)

A line-buffering variant keeps the TextMessage+one-line-per-message shape the docs describe; the important property is one message per chunk, not per entire session.


Important Issues

I1. Docker log reader and WebSocket goroutine leak on client disconnect Claude Gemini
		http.Error(w, "Invalid container ID", http.StatusBadRequest)
		return
	}

	// TODO: Figure out containerd
	reader, err := r.watcher.cli.ContainerLogs(
		req.Context(),
		containerID,
		client.ContainerLogsOptions{
			ShowStdout: true,
			ShowStderr: true,
			Follow:     true,
		},
	)
	if err != nil {
		switch {
		case errdefs.IsNotFound(err):
			log.V(5).Info("Container not found", "container", containerID)
			http.Error(w, "Container not found", http.StatusNotFound)
		case errdefs.IsInvalidArgument(err):
			log.V(5).Info("Invalid argument", "error", err)
			http.Error(w, "Invalid argument", http.StatusBadRequest)
		default:
			log.V(5).Info("Failed to get container logs", "error", err)
			http.Error(w, "Failed to get container logs", http.StatusInternalServerError)
		}
		return
	}

	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(w, req, nil)
	if err != nil {
		log.V(5).Info("Failed to upgrade to WebSocket", "error", err)
		return
	}
	defer conn.Close()
	defer func() {
		message := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Closing connection")
		err := conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

	writer, err := conn.NextWriter(websocket.TextMessage)
	if err != nil {
		log.V(5).Info("Failed to get next writer", "error", err)
		return
	}
	defer writer.Close()

	_, err = stdcopy.StdCopy(writer, writer, reader)
	if err != nil {
		log.V(5).Info("Failed to copy container logs", "error", err)
		// At this point we already sent the data, so we can't send HTTP status.
	}
}

The moby client's ContainerLogs contract states the caller must close the returned io.ReadCloser, but HandleLogs never calls reader.Close() — not on the Upgrade-failure path (lines 61-64) and not on the normal-return path. Once upgrader.Upgrade hijacks the connection, http.Server no longer cancels req.Context() when the remote TCP connection drops, so a client that drops the socket does not cancel the Docker request either, and the response body stays open until the Docker daemon evicts it.

Gorilla WebSocket additionally requires the server to run a concurrent conn.ReadMessage() loop to observe client-sent close frames. HandleLogs only writes, so on a clean client close frame StdCopy blocks forever on reader.Read for an idle container — a permanent goroutine + Docker-connection leak per request.

Fix: derive a cancellable context, defer reader.Close() right after the error check, and run a reader goroutine to convert WebSocket closure into context cancellation.

ctx, cancel := context.WithCancel(req.Context())
defer cancel()

reader, err := watcher.cli.ContainerLogs(ctx, ...)
if err != nil { ... return }
defer reader.Close()

conn, err := upgrader.Upgrade(w, req, nil)
if err != nil { ... return }
defer conn.Close()

go func() {
    defer cancel()
    for {
        if _, _, err := conn.ReadMessage(); err != nil {
            return
        }
    }
}()
I2. log.V(5) silences every error diagnostic Claude
		},
	)
	if err != nil {
		switch {
		case errdefs.IsNotFound(err):
			log.V(5).Info("Container not found", "container", containerID)
			http.Error(w, "Container not found", http.StatusNotFound)
		case errdefs.IsInvalidArgument(err):
			log.V(5).Info("Invalid argument", "error", err)
			http.Error(w, "Invalid argument", http.StatusBadRequest)
		default:
			log.V(5).Info("Failed to get container logs", "error", err)
			http.Error(w, "Failed to get container logs", http.StatusInternalServerError)
		}
		return
	}

	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(w, req, nil)
	if err != nil {
		log.V(5).Info("Failed to upgrade to WebSocket", "error", err)
		return
	}
	defer conn.Close()
	defer func() {
		message := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Closing connection")
		err := conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

	writer, err := conn.NextWriter(websocket.TextMessage)
	if err != nil {
		log.V(5).Info("Failed to get next writer", "error", err)
		return
	}
	defer writer.Close()

	_, err = stdcopy.StdCopy(writer, writer, reader)
	if err != nil {
		log.V(5).Info("Failed to copy container logs", "error", err)
		// At this point we already sent the data, so we can't send HTTP status.
	}
}

Every error branch logs at V(5), which requires -v=5/RDD_LOG_LEVEL=trace to surface. RDD runs at v=0 by default, so "container not found", Docker 5xx passthrough, WebSocket upgrade failures, and the post-upgrade copy failure are all invisible in rdd.stderr.log. The post-upgrade paths are the only channels where the client does not already see an HTTP body (once the connection is hijacked, http.Error cannot run), so a "logs stopped streaming" report has no server-side breadcrumb to investigate.

Keep V(5) for client-input validation ("Invalid container ID"), but raise the rest to log.Error / log.Info:

log.Error(err, "Failed to get container logs", "container", containerID)
http.Error(w, "Failed to get container logs", http.StatusInternalServerError)

The mock controller uses the same V(5) pattern, but it exists to not spam the test fixture; the real engine path deserves diagnosable failures.

I3. GetPassthroughEndpoints returns map keys in random order Gemini
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: SUSE LLC
// SPDX-FileCopyrightText: The Rancher Desktop Authors

// Package engine registers the engine controller. The engine controller
// mirrors Docker container engine state (containers, images, volumes)
// into `Container`, `Image`, and `Volume` resources in the
// containers.rancherdesktop.io API group, and forwards user-initiated
// deletions back to the Docker engine.
package engine

import (
	"runtime"

	ctrl "sigs.k8s.io/controller-runtime"

	appv1alpha1 "github.com/rancher-sandbox/rancher-desktop-daemon/pkg/apis/app/v1alpha1"
	containersv1alpha1 "github.com/rancher-sandbox/rancher-desktop-daemon/pkg/apis/containers/v1alpha1"
	"github.com/rancher-sandbox/rancher-desktop-daemon/pkg/controllers/app/engine/controllers"
	"github.com/rancher-sandbox/rancher-desktop-daemon/pkg/controllers/base"

Go randomises map iteration, and slices.Collect preserves that order. The result feeds EnabledPassthroughs in the discovery ConfigMap (manager.go:331-337, manager.go:346). Today the map has one key so the order is stable by accident; as soon as a second endpoint (exec, stats) is added, the serialised JSON will shuffle on every restart and produce spurious SSA updates on the discovery ConfigMap, which in turn wakes every watcher downstream. This is a latent, one-line fix now; after the second endpoint it becomes a noisy observable.

// registerDiscovery registers the controller manager information in the cluster.
func (scm *SharedControllerManager) registerDiscovery(ctx context.Context) error {
	// Get controller names, filtering out builtin controllers.
	// Builtin controllers are internal system controllers and should not be registered in discovery.
	var controllerNames []string
	passthroughEndpoints := make(map[string][]string)
	for _, registration := range scm.registrations {
		if registration.GetAPIGroup() != "builtin" {
			controllerNames = append(controllerNames, registration.GetName())
		}
		if httpController, ok := registration.(base.PassthroughController); ok {
			passthroughEndpoints[registration.GetName()] = httpController.GetPassthroughEndpoints()
		}
	}

	return scm.discovery.RegisterControllerManager(ctx, ControllerManagerInput{
		HealthPort:          scm.healthPort,
		MetricsPort:         scm.metricsPort,
		PassthroughPort:     scm.passthroughPort,
		EnabledControllers:  controllerNames,
		EnabledPassthroughs: passthroughEndpoints,
	})
}

// installControllerCRDs installs all controller CRDs in parallel for better performance.
func (scm *SharedControllerManager) installControllerCRDs(ctx context.Context) error {
func (c *controller) GetPassthroughEndpoints() []string {
    keys := slices.Collect(maps.Keys(c.passthroughs))
    slices.Sort(keys)
    return keys
}
I4. Design doc advertises the wrong URL and the wrong message shape Codex

The design doc says:

> An endpoint at /passthrough/containers/logs will speak WebSocket; messages are one way, text only, with each line being one message.

Two mismatches with the PR: (a) the controller registers under its name engine (controller.go:64-65), and the passthrough server mounts each endpoint at /{controller}/{endpoint} (manager.go:475-478), so the actual route is /passthrough/engine/logs, not /passthrough/containers/logs. Any consumer built from the doc will hit 404. (b) "one message per line" is the behaviour of the mock, not of the new Docker implementation (see C2). Once C2 is fixed, the message-shape sentence will be accurate again for TextMessage; the URL line needs updating regardless.

//go:embed crd.yaml
var appCRD string

const (
	// appValidatorWebhookName is the name used for the App validating webhook.
	appValidatorWebhookName = "app-validator.app.rancherdesktop.io"
	// appValidatorConfigName is the name of the App ValidatingWebhookConfiguration.
	appValidatorConfigName = "app-validator"
)

// controller implements the base.Controller interface for app.
type controller struct {
	for _, registration := range scm.registrations {
		if httpController, ok := registration.(base.PassthroughController); ok {
			hasPassthroughServers = true
			for _, endpoint := range httpController.GetPassthroughEndpoints() {
				handler := httpController.GetPassthroughHandler(endpoint)
				prefix := fmt.Sprintf("/%s/%s", registration.GetName(), endpoint)
				log.V(2).Info("Registering passthrough endpoint",
					"controller", registration.GetName(), "endpoint", prefix)
				mux.Handle(prefix+"/", http.StripPrefix(prefix, handler))
			}
		}
	}

	if !hasPassthroughServers {

Suggestions

S1. Use BinaryMessage for Docker log frames Claude
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

	writer, err := conn.NextWriter(websocket.TextMessage)
	if err != nil {
		log.V(5).Info("Failed to get next writer", "error", err)
		return
	}
	defer writer.Close()

	_, err = stdcopy.StdCopy(writer, writer, reader)
	if err != nil {
		log.V(5).Info("Failed to copy container logs", "error", err)
		// At this point we already sent the data, so we can't send HTTP status.
	}
}

stdcopy.StdCopy forwards the raw stdout/stderr bytes verbatim. Container stdout is frequently non-UTF-8 (binary output, ls --color=always, NUL bytes). The WebSocket spec requires TextMessage frames to be valid UTF-8, so a strict client will fail on the first non-UTF-8 payload. Covered implicitly by C2's proposed fix, but callable as a standalone fix if C2's framing is deferred.

S2. stdcopy.StdCopy(writer, writer, reader) merges stdout and stderr Claude Gemini

Passing the same writer for stdout and stderr strips the one structural signal Docker's multiplexed stream carries. Clients that colour stderr or tag it in the UI lose that affordance. Cheaper alternatives: forward the raw multiplexed reader to the client as BinaryMessage and let the client demux with stdcopy; or wrap each message in a small envelope ({"stream":"stdout","data":"..."} or a leading STREAM_TYPE byte). Worth deciding before a consumer depends on the merged shape.

S3. Duplicate container-ID regex and path parser with the mock controller Claude
	"github.com/moby/moby/client"

	ctrl "sigs.k8s.io/controller-runtime"
)

var containerIDValidator = regexp.MustCompile(`^[0-9a-fA-F]+$`)

// HandleLogs implements the log endpoint to pass through container logs.
func (r *EngineReconciler) HandleLogs(w http.ResponseWriter, req *http.Request) {
	log := ctrl.LoggerFrom(req.Context())
	containerID, _, _ := strings.Cut(strings.TrimLeft(req.URL.Path, "/"), "/")

pkg/controllers/mock/logs_passthrough.go:24 declares the identical regex, and both handlers use the same strings.Cut(strings.TrimLeft(req.URL.Path, "/"), "/") to extract the ID. A small helper in pkg/controllers/base (or a new pkg/controllers/passthrough) centralises the rule — important the first time we decide, e.g., to allow truncated IDs or names, so the decision is one edit.

	ctrl "sigs.k8s.io/controller-runtime"

	"github.com/rancher-sandbox/rancher-desktop-daemon/pkg/apis/containers/v1alpha1"
)

var containerIDValidator = regexp.MustCompile(`^[0-9a-fA-F]+$`)

// handleLogs is the handler for the `/passthrough/mock/logs/{container}`
// endpoint.  It exists for providing fake logs for testing and screenshots.
func (c *controller) handleLogs(w http.ResponseWriter, r *http.Request) {
	log := ctrl.LoggerFrom(r.Context())
S4. No tests for the new endpoint Claude Codex Gemini

No unit or BATS coverage lands with this PR. Minimum set:

  1. Unit test against a bare EngineReconciler{} (watcher nil) asserting 503 once C1 is fixed — pins the nil-watcher contract.
  2. Unit test with an invalid container ID asserting 400 without touching the watcher — pins the regex.
  3. BATS integration in bats/tests/32-app/ that boots a container, opens the endpoint, and asserts expected log bytes arrive on the WebSocket. This is the only coverage that catches C2 (session-buffering), I1 (goroutine growth under repeated open/close), and the mock/real divergence in frame shape.
S5. ContainerLogsOptions.Tail unset replays full history on every open Claude

	// TODO: Figure out containerd
	reader, err := r.watcher.cli.ContainerLogs(
		req.Context(),
		containerID,
		client.ContainerLogsOptions{
			ShowStdout: true,
			ShowStderr: true,
			Follow:     true,
		},
	)
	if err != nil {
		switch {
		case errdefs.IsNotFound(err):
			log.V(5).Info("Container not found", "container", containerID)

Tail defaulting to "all" means a container with a gigabyte of history dumps the whole log over the WebSocket before catching up. Accepting tail/since/timestamps from the query string with sensible defaults (e.g. tail=1000) matches Docker CLI conventions and costs ~15 lines. Fine to defer, but a TODO comment next to // TODO: Figure out containerd would pin it.

S6. HandleLogs docstring omits the path shape and framing Claude Gemini
	ctrl "sigs.k8s.io/controller-runtime"
)

var containerIDValidator = regexp.MustCompile(`^[0-9a-fA-F]+$`)

// HandleLogs implements the log endpoint to pass through container logs.
func (r *EngineReconciler) HandleLogs(w http.ResponseWriter, req *http.Request) {
	log := ctrl.LoggerFrom(req.Context())
	containerID, _, _ := strings.Cut(strings.TrimLeft(req.URL.Path, "/"), "/")
	log.Info("Handling logs for container", "containerID", containerID)

The mock's handler docstring names the path shape (/passthrough/mock/logs/{container}) — the production handler should be at least as explicit about URL shape, expected container-ID form (hex only, names rejected), WebSocket upgrade, and the frame type clients must accept.


Design Observations

Concerns

Strengths


Testing Assessment

No unit or BATS tests are added. All three agents flagged this. Ranked by risk:

  1. Nil-watcher path (C1) — a unit test on EngineReconciler{} asserting 503 pins the contract and blocks the regression from returning.
  2. WebSocket happy path (C2, I1, S1) — a BATS integration that opens the endpoint against a real container is the only way to catch session-buffering, text/binary framing, and per-request goroutine leaks.
  3. Error branches — recorder-backed fake asserting 400 on InvalidArgument and 404 on NotFound, and checking that nothing is logged at v=0 today (i.e. the I2 regression guard once I2 is fixed).

Documentation Assessment


Commit Structure

Single commit, single concept, message matches intent. No issues.


Acknowledged Limitations


Unresolved Feedback

None. No PR review comments on round 1.


Agent Performance Retro

Claude

Delivered the most complete single review: caught C1 with the sharpest framing (both nil panic and data race, with specific reference sites), owned I1 (reader leak), and was the only agent to flag I2 (V(5) silences diagnostics) — a finding that requires domain knowledge of RDD's default logger verbosity. Produced six suggestions spanning framing, duplication, ergonomics, history replay, and documentation. Missed C2 as a top-line critical; did note the TextMessage UTF-8 issue as S1 but did not identify the single-message buffering problem behind it, which is the dominant bug in the streaming path.

Codex

Tight, verdict-focused review with only the two highest-impact findings. Landed C2 with the clearest mechanism (NextWriter's FIN bit, contract mismatch with the design doc and the mock implementation) and independently confirmed I1's nil-watcher hazard by tracing every site that clears r.watcher. Uniquely caught the design-doc URL mismatch (I4), which neither of the other agents surfaced. Rated the nil-watcher issue Important where Claude and Gemini rated it Critical — the consolidation keeps the higher severity because a panic on any non-moby or stopped state is a shipped crash path, not a theoretical race.

Gemini

Three criticals, one important. Got C1 and C2 directly, and was the only agent to surface I3 (map-iteration-randomised passthrough list) — a latent one-line fix that would otherwise land as a flaky CI symptom after a second endpoint arrives. Its C3 (goroutine leak / missing ReadMessage loop) strengthened Claude's narrower reader-close finding; consolidated as I1 with Gemini's concurrent-reader framing. Fewest suggestions of the three; no S-level findings at all. Did not run git blame (per the known daily-quota gap), so pre-existing-vs-regression calls come from Claude and Codex.

Summary

Claude Opus 4.7Codex GPT 5.4Gemini 3.1 Pro
Duration18m 27s5m 52s4m 07s
Findings1C 2I 6S2C 1I2C 2I
Tool calls22 (Read 11, Grep 6, Bash 5)47 (shell 44, stdin 3)26 (grepsearch 10, readfile 7, runshellcommand 7)
Design observations401
False positives000
Unique insights211
Files reviewed222
Coverage misses000
Totals1C 2I 6S2C 1I2C 2I
Downgraded001 (I→S)
Dropped000

Reconciliation. Codex P1 nil-watcher panic: important → critical (consolidated into C1; a production-path nil panic is a shipped crash, not a theoretical race). Gemini P1 goroutine leak on client disconnect: critical → important (consolidated into I1; reader-close is a clear leak, but the full hang only occurs on a clean client-close with an idle container — the more common torn-TCP case still unwinds via failed writes, so "important" fits the observed blast radius).

Overall, Codex and Gemini converged on the two blockers (C1, C2) with independent reasoning, which made the consolidated verdict easy to defend. Claude carried most of the long tail — the reader leak, the logging level, the streaming-style suggestions, the shared-pattern cleanup — and caught the most design observations. Each agent contributed at least one finding the others missed (Claude: I2; Codex: I4; Gemini: I3).


Review Process Notes

Skill improvements

None this round — every finding was within existing dimensions.

Repo context updates

None this round.