GitoxideLabs/gitoxide · error
rebase worker stopped unexpectedly
Error message
rebase worker stopped unexpectedly
What it means
During interactive rebase rendering, the driver thread waits on a bounded `recv_timeout` for events from the rebase worker thread. A `mpsc::RecvTimeoutError::Disconnected` means every sender handle to the worker's event channel has been dropped, i.e. the worker thread exited without reporting `Complete`. The library raises this because a silent worker shutdown would otherwise look like a missing timeout event.
Solutions
- Inspect the worker closure for early `return`/`break`/`?` paths that exit without sending `RebaseWorkerEvent::Complete`; make every exit path send a terminal event or propagate the error before dropping the sender.
- Keep a clone of the `mpsc::Sender` alive for the worker's full lifetime (e.g. in a guard that sends `Complete`/error on drop).
- Check whether the rebase script/todos are malformed, causing the worker to bail out before producing events.
- Reproduce with logging inside the worker to see why the thread exits early.
Example fix
// before
match receiver.recv_timeout(timeout) {
Ok(event) => Some(event),
Err(mpsc::RecvTimeoutError::Timeout) => None,
Err(mpsc::RecvTimeoutError::Disconnected) => {
break Err(anyhow::anyhow!("rebase worker stopped unexpectedly"))
}
}
// after
// In the worker: ensure the terminal event is always sent,
// even on error paths:
let result = run_rebase_steps(&mut progress_tx);
let _ = result_tx.send(RebaseWorkerEvent::Complete(result)); Defensive patterns
Strategy: try-catch
Validate before calling
// Before driving the worker, verify the channel contract: // keep a Sender clone alive and assert the worker emits a terminal event let keepalive = result_tx.clone(); // drop only after worker.join() let _ = keepalive;
Type guard
fn worker_terminated(result: &Result<(), anyhow::Error>) -> bool {
result.is_ok() || !result.as_ref().unwrap_err().to_string().contains("stopped unexpectedly")
} Try / catch
match run_rebase_with_tui(...) {
Err(e) if e.to_string().contains("rebase worker stopped unexpectedly") => {
// worker exited early without Complete; inspect worker logs / retry
}
Err(e) => return Err(e),
Ok(v) => return Ok(v),
} Prevention
- Always send a terminal `Complete` event on every worker exit path, including errors
- Use a drop guard in the worker so a Sender is never silently dropped
- Test the worker with malformed rebase todos to exercise early-exit paths
- Keep a Sender clone alive until after `join()`
When it happens
Trigger: Calling the interactive rebase-with-TUI flow (the function driving `RebaseWorkerEvent`s at gix-tix/src/lib.rs:5840) when the worker thread returns early or its spawning/pump loop drops the `mpsc::Sender` before sending `Complete`.
Common situations: The worker hit an early `return`/`break` on an unrecoverable rebase error and dropped its sender; the user-initiated abort path closed the channel without sending a completion event; a bug in the worker loop exited without `result_tx.send(...)`.
Related errors
- rebase worker panicked
- Thread failed to send result
- rebase todo requires at least one -x/--hide revision when…
- the hidden and visible revisions have no editable fork point
- the revisions have multiple editable fork points
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/a87e7adfa3c49057.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/lib.rs:5840
let started = Instant::now();
let mut last_draw = started;
let mut latest = None;
let mut rendered = None;
let result = loop {
let now = Instant::now();
let timeout = if now.duration_since(started) < TODO_PROGRESS_DELAY {
Some(TODO_PROGRESS_DELAY.saturating_sub(now.duration_since(started)))
} else if latest != rendered {
Some(FRAME_INTERVAL.saturating_sub(now.duration_since(last_draw)))
} else {
None
};
let event = match timeout {
Some(timeout) => match receiver.recv_timeout(timeout) {
Ok(event) => Some(event),
Err(mpsc::RecvTimeoutError::Timeout) => None,
Err(mpsc::RecvTimeoutError::Disconnected) => {
break Err(anyhow::anyhow!("rebase worker stopped unexpectedly"));
}
},
None => match receiver.recv() {
Ok(event) => Some(event),
Err(_) => break Err(anyhow::anyhow!("rebase worker stopped unexpectedly")),
},
};
match event {
Some(RebaseWorkerEvent::Progress(progress)) => latest = Some(progress),
Some(RebaseWorkerEvent::Complete(result)) => break result,
None => {}
}
let now = Instant::now();
if todo_progress_visible(now.duration_since(started))
&& latest != rendered
&& now.duration_since(last_draw) >= FRAME_INTERVAL
{
let progress = latest.expect("a changed progress snapshot is available");View on GitHub (pinned to e73179060b)