Hmbown/CodeWhale · error · anyhow::Error

lane `{}` was stopped before tmux dry-run start completed

Error message

lane `{}` was stopped before tmux dry-run start completed

What it means

In the CODEWHALE_LANE_TMUX_DRY_RUN test path, TmuxRuntime::start records a lane_log event and then calls registry.mark_running_if_pending(record). A false return means the lane's status is no longer Pending — a concurrent stop already claimed it — so the start aborts rather than flipping a stopped lane back to Running.

Source

Thrown at crates/lane/src/runtime.rs:729

                "workflow": record.workflow,
                "fleet": record.fleet,
                "issue": record.issue,
                "dry_run": dry_run,
            }),
        )?;

        if dry_run {
            append_log_event(
                &record.log_path,
                serde_json::json!({
                    "type": "lane_log",
                    "message": "tmux dry-run: session recorded without spawning process",
                    "command": spec.command,
                    "cwd": cwd.as_ref().map(|p| p.display().to_string()),
                }),
            )?;
            if !registry.mark_running_if_pending(record)? {
                bail!(
                    "lane `{}` was stopped before tmux dry-run start completed",
                    record.id
                );
            }
            return Ok(());
        }

        let log_proxy = spec
            .log_proxy
            .as_deref()
            .context("tmux runtime requires a lane log proxy executable")?;

        // Detached session: child output remains an operator journal only.
        // Terminal control state goes through a separate bounded, atomically
        // renamed receipt so child stdout cannot forge Lane completion.
        let receipt_path = lane_exit_receipt_path(&record.log_path);
        let receipt_tmp_path = lane_exit_receipt_tmp_path(&record.log_path);
        let environment_path = lane_environment_path(&record.log_path);

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Treat the lane as stopped: refetch its record and assert the terminal state instead of retrying the start
  2. Serialize start/stop per lane id in tests (await the start's completion before stopping)
  3. Use distinct lane ids per test to avoid registry contention

Example fix

// before
start(lane_id);
stop(lane_id); // may win the race in dry-run tests

// after
start(lane_id).await?; // wait for the Pending->Running transition
stop(lane_id).await?;
Defensive patterns

Strategy: try-catch

Try / catch

match runtime.start(&registry, &mut record, &spec) {
    Ok(()) => { /* lane running (dry-run recorded) */ }
    Err(err) if err.to_string().contains("stopped before") => {
        let fresh = registry.get(&lane_id)?; // refetch: lane is stopped, not failed
        assert_eq!(fresh.status, LaneStatus::Stopped);
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Dry-run lane start racing a concurrent stop (or a registry transition) on the same lane id: by the time the Pending->Running compare-and-set runs, the state has moved. Only occurs with CODEWHALE_LANE_TMUX_DRY_RUN set, i.e. in tests.

Common situations: Parallel tests starting and stopping the same lane id; test code issuing stop immediately after start without waiting for the transition; shared registry state across test tasks.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/327cac93b0dd1d8f. Report an issue: GitHub.