Hmbown/CodeWhale · error

Failed to sync thread session

Error message

Failed to sync thread session: {e}

What it means

Raised while loading a thread when restoring its persisted history onto a freshly spawned engine fails: the runtime sends an `Op::SyncSession` message (restored session messages, optional system prompt, model, workspace, mode) and the engine's channel send returns Err. That happens only when the engine's receiver is gone — the just-spawned engine task already terminated — or the send otherwise fails with error `{e}`.

Solutions

  1. Read `{e}` for the underlying send failure; check logs for an engine startup panic and fix its config cause.
  2. Retry opening the thread — a new engine is spawned on the next attempt.
  3. Validate tools/advisor/exec-policy settings in the config that the engine consumes at startup.
  4. Avoid opening threads during application shutdown; free memory if OOM killed the worker.
Defensive patterns

Strategy: retry

Try / catch

match open_thread(&thread_id).await {
    Err(e) if e.to_string().contains("Failed to sync thread session") => {
        // engine died at startup: check logs, fix config, then reopen the thread
    }
    r => r?,
}

Prevention

When it happens

Trigger: Opening/reloading a thread whose restored session messages or system prompt are non-empty, when `engine.send(Op::SyncSession {...}).await` returns Err because the engine worker died immediately after spawn (panic at startup, invalid engine config) or the channel was closed.

Common situations: Engine worker panics during initialization (bad tools config, invalid exec policy engine); app shutting down while threads are being opened; resource exhaustion (OOM) killing the freshly spawned task; a config reload spawning an engine that fails to start.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/4b35b8c10ccae946. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:11084

                .map(|s| SystemPrompt::Text(s.clone()));
            if !session_messages.is_empty() || sys_prompt.is_some() {
                engine
                    .send(Op::SyncSession {
                        session_id: thread.session_id.clone(),
                        messages: session_messages,
                        system_prompt: sys_prompt,
                        system_prompt_override: thread.system_prompt.is_some(),
                        model: route_model.clone(),
                        workspace: thread.workspace.clone(),
                        mode: RuntimePolicyProjection::from_persisted(
                            &thread.mode,
                            thread.permission_posture.as_deref(),
                            thread.auto_approve,
                        )
                        .mode,
                    })
                    .await
                    .map_err(|e| anyhow!("Failed to sync thread session: {e}"))?;
            }

            let mut active = self.active.lock().await;
            if let Some(winner) = active
                .engines
                .get(&thread.id)
                .map(|state| state.engine.clone())
            {
                touch_lru(&mut active.lru, &thread.id);
                drop(active);
                engine.cancel_with_reason(crate::core::engine::CancelReason::Internal);
                let _ = engine.try_send(Op::Shutdown);
                return Ok(winner);
            }

            // Atomically compare the record used for construction with the latest
            // durable record while holding the same active -> thread lock order as
            // updates. A concurrent workspace/model/session/policy change makes

View on GitHub (pinned to 73e0f67d83)