{"record":{"id":"340401b15e85ab73","repo":"Hmbown/CodeWhale","slug":"thread-message-for-thread-id-cannot-be-executed-here-runtime","errorCode":null,"errorMessage":"thread message for {thread_id} cannot be executed here: Runtime::handle_thread does not run turns. Send it through the app-server runtime bridge (POST /v1/threads/{id}/turns).","messagePattern":"thread message for (.+?) cannot be executed here: Runtime::handle_thread does not run turns\\. Send it through the app-server runtime bridge \\(POST /v1/threads/(.+?)/turns\\)\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/core/src/lib.rs","lineNumber":1313,"sourceCode":"                    threads: Vec::new(),\n                    goal: None,\n                    model: None,\n                    model_provider: None,\n                    cwd: None,\n                    approval_policy: None,\n                    sandbox: None,\n                    events: Vec::new(),\n                    data: json!({}),\n                })\n            }\n            // A thread message is a *turn*, and this type is not the turn\n            // engine — it owns thread bookkeeping and persistence only. The\n            // app-server routes messages through its runtime bridge\n            // (`POST /v1/threads/{id}/turns` on the runtime API) and never\n            // reaches this arm. Returning an error rather than a canned\n            // \"accepted\" keeps any other caller from mistaking bookkeeping\n            // for execution.\n            ThreadRequest::Message { thread_id, .. } => Err(anyhow!(\n                \"thread message for {thread_id} cannot be executed here: \\\n                 Runtime::handle_thread does not run turns. Send it through the \\\n                 app-server runtime bridge (POST /v1/threads/{{id}}/turns).\"\n            )),\n        }\n    }\n\n    /// Evaluates execution policy and dispatches a tool call.\n    pub async fn invoke_tool(\n        &self,\n        call: ToolCall,\n        approval_mode: AskForApproval,\n        cwd: &Path,\n    ) -> Result<Value> {\n        let fallback_cwd = cwd.display().to_string();\n        let (command, policy_cwd, execution_kind) = call.execution_subject(&fallback_cwd);\n        let policy_tool = match &call.payload {\n            ToolPayload::LocalShell { .. } => \"exec_shell\",","sourceCodeStart":1295,"sourceCodeEnd":1331,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/core/src/lib.rs#L1295-L1331","documentation":"Runtime::handle_thread in crates/core is bookkeeping-only: it owns thread metadata and persistence, never turn execution. Receiving ThreadRequest::Message on this runtime means a caller tried to run a model turn through the wrong engine; instead of faking an 'accepted' response it returns this error directing the caller to the app-server runtime bridge endpoint POST /v1/threads/{id}/turns.","triggerScenarios":"Sending a ThreadRequest::Message (a turn/message execution request for the given thread_id) directly into the core Runtime's handle_thread instead of routing it through the app-server HTTP runtime bridge (POST /v1/threads/{id}/turns).","commonSituations":"A client or plugin called the core runtime API directly instead of the app-server; an internal refactor moved message routing off the bridge; tooling replayed a message against the persistence runtime during recovery or tests.","solutions":["Route the message through the app-server runtime bridge: POST to /v1/threads/{thread_id}/turns instead of invoking Runtime::handle_thread with ThreadRequest::Message.","Update your client integration to use the app-server's thread-turn endpoint rather than the core bookkeeping API.","If you only need thread bookkeeping (metadata/persistence), keep using handle_thread but do not send Message variants through it.","If you believe you are already going through the bridge, verify which runtime handle your client is bound to; you are likely holding the core runtime reference directly."],"exampleFix":"// before\ncore_runtime.handle_thread(ThreadRequest::Message { thread_id, message })?;\n// after\napp_server.post(&format!(\"/v1/threads/{thread_id}/turns\"), &turn_payload)?;","handlingStrategy":"try-catch","validationCode":"// Before sending, ensure the request targets the app-server bridge, not core runtime\nif let ThreadRequest::Message { thread_id, .. } = req {\n    let url = format!(\"{app_server_base}/v1/threads/{thread_id}/turns\");\n    return http.post(&url).json(&turn_payload).send();\n}","typeGuard":"fn is_bookkeeping_only(req: &ThreadRequest) -> bool {\n    matches!(req, ThreadRequest::Message { .. }) // must not go to handle_thread\n}","tryCatchPattern":"match runtime.handle_thread(req) {\n    Err(e) if e.to_string().contains(\"does not run turns\") => {\n        // reroute through the app-server runtime bridge\n        bridge_post_turns(req)?;\n    }\n    other => other?,\n}","preventionTips":["Always route turn execution through POST /v1/threads/{id}/turns on the app-server, never through the core runtime handle.","Keep a single client wrapper that owns the bridge URL so callers cannot grab a core runtime handle directly.","In tests, use the bridge client rather than Runtime::handle_thread for Message requests."],"tags":["architecture","api-routing","threads","runtime"],"backgroundTag":"unsupported-operation","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"}