{"record":{"id":"871baa7f90f12691","repo":"Hmbown/CodeWhale","slug":"at-least-one-thread-field-is-required","errorCode":null,"errorMessage":"At least one thread field is required","messagePattern":"At least one thread field is required","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":4639,"sourceCode":"        self.flush_recovery_receipts_for_thread(id).await?;\n        self.store\n            .load_thread(id)\n            .with_context(|| format!(\"Thread not found: {id}\"))\n    }\n\n    pub async fn update_thread(&self, id: &str, req: UpdateThreadRequest) -> Result<ThreadRecord> {\n        if req.archived.is_none()\n            && req.allow_shell.is_none()\n            && req.trust_mode.is_none()\n            && req.auto_approve.is_none()\n            && req.model.is_none()\n            && req.mode.is_none()\n            && req.permission_posture.is_none()\n            && req.title.is_none()\n            && req.system_prompt.is_none()\n            && req.workspace.is_none()\n        {\n            bail!(\"At least one thread field is required\");\n        }\n\n        if let Some(model) = req.model.as_ref()\n            && model.trim().is_empty()\n        {\n            bail!(\"model must not be empty\");\n        }\n        if let Some(mode) = req.mode.as_ref()\n            && mode.trim().is_empty()\n        {\n            bail!(\"mode must not be empty\");\n        }\n        if let Some(permission_posture) = req.permission_posture.as_ref()\n            && permission_posture.trim().is_empty()\n        {\n            bail!(\"permission_posture must not be empty\");\n        }\n        if let Some(workspace) = req.workspace.as_ref()","sourceCodeStart":4621,"sourceCodeEnd":4657,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L4621-L4657","documentation":"update_thread was called with an UpdateThreadRequest in which every optional field (archived, allow_shell, trust_mode, auto_approve, model, mode, permission_posture, title, system_prompt, workspace) is None. The API rejects no-op patches instead of performing a load-and-save cycle that would emit an empty change record.","triggerScenarios":"Constructing UpdateThreadRequest::default() (or an all-None deserialization from an empty JSON PATCH body) and calling update_thread. Check at runtime_threads.rs:4630-4640.","commonSituations":"A client builds the request from a diff object that came up empty; serde deserializes {} into all-None; a UI 'save' button fires with no changed fields.","solutions":["Only send fields that actually changed - diff against the thread record client-side first","Skip the update call entirely when the change set is empty","If using PATCH-style JSON, ensure at least one key is present before sending","Check the struct after deserialization and reject empty bodies at your own API boundary with a clearer message"],"exampleFix":"// before\nlet req = UpdateThreadRequest::default();\nmanager.update_thread(id, req).await?; // bails: no fields\n\n// after\nlet mut req = UpdateThreadRequest::default();\nreq.title = Some(\"New title\".to_string());\nmanager.update_thread(id, req).await?;","handlingStrategy":"validation","validationCode":"// Diff before patching: skip empty requests.\nfn has_any_field(req: &UpdateThreadRequest) -> bool {\n    req.archived.is_some() || req.allow_shell.is_some() || req.trust_mode.is_some()\n        || req.auto_approve.is_some() || req.model.is_some() || req.mode.is_some()\n        || req.permission_posture.is_some() || req.title.is_some()\n        || req.system_prompt.is_some() || req.workspace.is_some()\n}\nif !has_any_field(&req) {\n    return Ok(current_record); // no-op: nothing to update\n}\nmanager.update_thread(id, req).await?;","typeGuard":"fn is_empty_update(req: &UpdateThreadRequest) -> bool {\n    req.archived.is_none() && req.allow_shell.is_none() && req.trust_mode.is_none()\n        && req.auto_approve.is_none() && req.model.is_none() && req.mode.is_none()\n        && req.permission_posture.is_none() && req.title.is_none()\n        && req.system_prompt.is_none() && req.workspace.is_none()\n}","tryCatchPattern":null,"preventionTips":["Build the request from a client-side diff of changed fields, not a defaults-initialized struct","Reject empty PATCH bodies at your own API boundary with a clearer message","Gate 'save' actions in UIs on at least one changed field"],"tags":["thread-management","request-validation","patch","empty-request"],"backgroundTag":"empty-patch-request","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}