Hmbown/CodeWhale · error

thread permissions changed during update; refresh the…

Error message

thread permissions changed during update; refresh the conversation before trying again

What it means

During a shell/workspace permission update, the library rechecks the thread's conversation identity after loading shell policy (which reads config files off the async runtime). If the effective workspace implied by the request no longer matches the reloaded thread state, it assumes permissions changed concurrently and aborts the grant.

Solutions

  1. Re-read the thread (refresh the conversation) to get current state, then rebuild and resend the request.
  2. Serialize updates to the same thread: use one client/lock or retry with backoff on this error.
  3. Ensure the request's workspace matches the thread's current workspace when only toggling allow_shell.
  4. Avoid editing the underlying config files while permission updates are in flight.

Example fix

// before
api.update_thread(id, req).await?; // stale view -> optimistic-concurrency error

// after
let fresh = api.get_thread(id).await?;
req.workspace = Some(fresh.workspace.clone()); // align with current state
api.update_thread(id, req).await?;
Defensive patterns

Strategy: retry

Validate before calling

let fresh = api.get_thread(id).await?;
if req.workspace.as_deref().is_some() && req.workspace.as_deref() != Some(fresh.workspace.as_str()) {
    req.workspace = Some(fresh.workspace.clone()); // resync before granting shell
}

Try / catch

match api.update_thread(id, req).await {
    Err(e) if e.to_string().contains("changed during update") => {
        let fresh = api.get_thread(id).await?;
        let mut req = rebuild_request(&fresh);
        api.update_thread(id, req).await?; // retry with fresh state
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling update-thread with `allow_shell` set (or `workspace` changed) while another actor concurrently modified the thread's workspace or permission state between the policy load and the commit — the recheck `shell_policy_workspace != req.workspace.unwrap_or(&thread.workspace)` fails.

Common situations: Two clients (or a UI plus an agent) update the same thread's workspace/permissions simultaneously; a config file was edited between the two internal steps; a retried request after a prior partially-applied change.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

            // holding the same active + record locks used by turn admission.
            if req.allow_shell == Some(true)
                && !thread.allow_shell
                && active
                    .engines
                    .get(id)
                    .and_then(|state| state.active_turn.as_ref())
                    .is_some()
            {
                bail!(
                    "thread '{id}' already has an active turn; finish it before enabling shell commands"
                );
            }
            if req.allow_shell.unwrap_or(thread.allow_shell)
                && (req.allow_shell.is_some() || req.workspace.is_some())
                && shell_policy_workspace.as_deref()
                    != Some(req.workspace.as_deref().unwrap_or(&thread.workspace))
            {
                bail!(
                    "thread permissions changed during update; refresh the conversation before trying again"
                );
            }
            let mut changes = serde_json::Map::new();
            let policy_patch = if req.mode.is_some()
                || req.permission_posture.is_some()
                || req.auto_approve.is_some()
            {
                Some(runtime_policy_with_overrides(
                    &thread,
                    req.mode.as_deref(),
                    req.permission_posture.as_deref(),
                    req.auto_approve,
                )?)
            } else {
                None
            };

View on GitHub (pinned to 73e0f67d83)