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
- Re-read the thread (refresh the conversation) to get current state, then rebuild and resend the request.
- Serialize updates to the same thread: use one client/lock or retry with backoff on this error.
- Ensure the request's workspace matches the thread's current workspace when only toggling allow_shell.
- 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
- Serialize permission updates per thread (single writer or lock)
- Always re-read the thread immediately before granting shell access
- Avoid concurrent edits to the thread's config/workspace sources
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
- config changed after it was loaded; reload
- {}
- thread ' ' already has an active turn; finish it before…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
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)