affaan-m/ECC · error

DB writer channel closed

Error message

DB writer channel closed

What it means

SessionStateHandle::send forwards DB mutations over an mpsc channel to a dedicated writer task; if that task has exited, the UnboundedSender fails and this error is returned to update_state/update_pid/append_output_line/touch_heartbeat callers. It means the session's persistence pipeline is dead — no further DB writes will land.

Solutions

  1. Recreate/reopen the DB writer (restart the session runtime) — the current handle cannot recover
  2. Guard callers so they stop issuing writes once the session is stopped/torn down
  3. Check the writer's open_error / stderr log for why run_db_writer exited (disk full, permissions, lock)

Example fix

// before
db_writer.append_output_line(line).await?; // panics/errors if writer dropped
// after
if db_writer.is_closed() {
    eprintln!("db writer unavailable; skipping output line");
} else {
    db_writer.append_output_line(line).await?;
}
Defensive patterns

Strategy: retry

Validate before calling

if db_writer.is_closed() {
    anyhow::bail!("db writer stopped; restart the session runtime before writing");
}

Try / catch

if let Err(e) = db_writer.touch_heartbeat().await {
    if e.to_string().contains("channel closed") {
        eprintln!("persistence pipeline dead; recreating writer...");
        db_writer = recreate_writer(&db_path, &session_id).await?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: The run_db_writer task panicked or returned (e.g. fatal open error on the SQLite file) while the session handle is still used; writing DB messages after the writer was dropped at session teardown.

Common situations: Corrupt or locked session DB causing the writer to exit; calling touch_heartbeat/append_output_line after stop_session dropped the writer; DB directory deleted at runtime.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/3d99a74b723f2066. Report an issue: GitHub.

Appendix: source

Thrown at ecc2/src/session/runtime.rs:71

    }

    async fn append_output_line(&self, stream: OutputStream, line: String) -> Result<()> {
        self.send(|ack| DbMessage::AppendOutputLine { stream, line, ack })
            .await
    }

    async fn touch_heartbeat(&self) -> Result<()> {
        self.send(|ack| DbMessage::TouchHeartbeat { ack }).await
    }

    async fn send<F>(&self, build: F) -> Result<()>
    where
        F: FnOnce(oneshot::Sender<DbAck>) -> DbMessage,
    {
        let (ack_tx, ack_rx) = oneshot::channel();
        self.tx
            .send(build(ack_tx))
            .map_err(|_| anyhow::anyhow!("DB writer channel closed"))?;

        match ack_rx.await {
            Ok(Ok(())) => Ok(()),
            Ok(Err(error)) => Err(anyhow::anyhow!(error)),
            Err(_) => Err(anyhow::anyhow!("DB writer acknowledgement dropped")),
        }
    }
}

fn run_db_writer(db_path: PathBuf, session_id: String, mut rx: mpsc::UnboundedReceiver<DbMessage>) {
    let (opened, open_error) = match StateStore::open(&db_path) {
        Ok(db) => (Some(db), None),
        Err(error) => (None, Some(error.to_string())),
    };

    while let Some(message) = rx.blocking_recv() {
        match message {
            DbMessage::UpdateState { state, ack } => {

View on GitHub (pinned to 8321021c54)