Hmbown/CodeWhale · error · anyhow::Error

lane exit receipt {} belongs to {}, expected {}

Error message

lane exit receipt {} belongs to {}, expected {}

What it means

After parsing, read_lane_exit_receipt checks receipt.lane_id against the lane id it was asked about and bails on mismatch. The receipt is only meaningful for the lane that wrote it; a mismatch means the file at lane_exit_receipt_path(log_path) is stale — left by a previous lane that used the same log path. Acting on it would attribute another lane's exit code to this lane.

Source

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

fn read_lane_exit_receipt(log_path: &Path, lane_id: &str) -> Result<Option<LaneExitReceipt>> {
    let path = lane_exit_receipt_path(log_path);
    let metadata = match std::fs::metadata(&path) {
        Ok(metadata) => metadata,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(err).with_context(|| format!("stat {}", path.display())),
    };
    if metadata.len() > MAX_EXIT_RECEIPT_BYTES {
        bail!(
            "lane exit receipt {} exceeds {} bytes",
            path.display(),
            MAX_EXIT_RECEIPT_BYTES
        );
    }
    let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
    let receipt: LaneExitReceipt =
        serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display()))?;
    if receipt.lane_id != lane_id {
        bail!(
            "lane exit receipt {} belongs to {}, expected {}",
            path.display(),
            receipt.lane_id,
            lane_id
        );
    }
    Ok(Some(receipt))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TmuxSessionState {
    Present,
    Absent,
}

fn tmux_command(socket: &Path) -> Command {
    let mut command = Command::new("tmux");
    command.arg("-S").arg(socket);

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Remove the stale receipt file named in the message, then reconcile again
  2. Give each lane generation a unique log path (embed the lane id or a run id)
  3. If you manage lifecycle code, clear receipt files when a lane's log directory is recycled

Example fix

// before
let log_path = logs_dir.join("lane-current.jsonl"); // reused across lanes

// after
let log_path = logs_dir.join(format!("{lane_id}.jsonl")); // unique per lane
Defensive patterns

Strategy: fallback

Validate before calling

// Use unique log paths per lane so receipts can never be cross-read.
let log_path = logs_dir.join(format!("{lane_id}.jsonl"));

Try / catch

match read_lane_exit_receipt(&log_path, &lane_id) {
    Ok(Some(receipt)) => { /* exit code for this lane */ }
    Err(err) if err.to_string().contains("belongs to") => {
        // Stale receipt from another lane: discard and fall back to tmux state.
        let _ = std::fs::remove_file(lane_exit_receipt_path(&log_path));
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling reconciliation for lane B while the receipt file under B's log_path still contains {"lane_id": "A", ...}. Happens when log paths are reused across lanes or generations and the old receipt was not removed.

Common situations: Regenerating lanes in a fixed directory (e.g. logs/lane-0) without cleanup; copying/cloning a workspace with leftover lane state; a lane id scheme that collides after truncation.

Related errors


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