{"record":{"id":"25ec06aa3ccc5815","repo":"Hmbown/CodeWhale","slug":"thread-permissions-changed-during-update-refresh-the","errorCode":null,"errorMessage":"thread permissions changed during update; refresh the conversation before trying again","messagePattern":"thread permissions changed during update; refresh the conversation before trying again","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7945,"sourceCode":"            // holding the same active + record locks used by turn admission.\n            if req.allow_shell == Some(true)\n                && !thread.allow_shell\n                && active\n                    .engines\n                    .get(id)\n                    .and_then(|state| state.active_turn.as_ref())\n                    .is_some()\n            {\n                bail!(\n                    \"thread '{id}' already has an active turn; finish it before enabling shell commands\"\n                );\n            }\n            if req.allow_shell.unwrap_or(thread.allow_shell)\n                && (req.allow_shell.is_some() || req.workspace.is_some())\n                && shell_policy_workspace.as_deref()\n                    != Some(req.workspace.as_deref().unwrap_or(&thread.workspace))\n            {\n                bail!(\n                    \"thread permissions changed during update; refresh the conversation before trying again\"\n                );\n            }\n            let mut changes = serde_json::Map::new();\n            let policy_patch = if req.mode.is_some()\n                || req.permission_posture.is_some()\n                || req.auto_approve.is_some()\n            {\n                Some(runtime_policy_with_overrides(\n                    &thread,\n                    req.mode.as_deref(),\n                    req.permission_posture.as_deref(),\n                    req.auto_approve,\n                )?)\n            } else {\n                None\n            };\n","sourceCodeStart":7927,"sourceCodeEnd":7963,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L7927-L7963","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\napi.update_thread(id, req).await?; // stale view -> optimistic-concurrency error\n\n// after\nlet fresh = api.get_thread(id).await?;\nreq.workspace = Some(fresh.workspace.clone()); // align with current state\napi.update_thread(id, req).await?;","handlingStrategy":"retry","validationCode":"let fresh = api.get_thread(id).await?;\nif req.workspace.as_deref().is_some() && req.workspace.as_deref() != Some(fresh.workspace.as_str()) {\n    req.workspace = Some(fresh.workspace.clone()); // resync before granting shell\n}","typeGuard":null,"tryCatchPattern":"match api.update_thread(id, req).await {\n    Err(e) if e.to_string().contains(\"changed during update\") => {\n        let fresh = api.get_thread(id).await?;\n        let mut req = rebuild_request(&fresh);\n        api.update_thread(id, req).await?; // retry with fresh state\n    },\n    other => other?,\n}","preventionTips":["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"],"tags":["concurrency","optimistic-locking","permissions"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}