{"record":{"id":"187334f604f3a066","repo":"aaif-goose/goose","slug":"state-machine-conversation-has-no-kickoff-message","errorCode":null,"errorMessage":"state machine conversation has no kickoff message","messagePattern":"state machine conversation has no kickoff message","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/goose-agent/src/operation.rs","lineNumber":29,"sourceCode":"use rmcp::model::Tool;\n\npub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;\n\npub struct SlashCommand<'a> {\n    pub command: &'a str,\n    pub params_str: &'a str,\n}\n\npub fn messages_since_kickoff(conversation: &Conversation) -> Result<&[Message]> {\n    let messages = conversation.messages();\n    let start = messages\n        .iter()\n        .rposition(|message| {\n            message.role == rmcp::model::Role::User\n                && message.is_user_visible()\n                && !message.is_tool_response()\n        })\n        .ok_or_else(|| anyhow!(\"state machine conversation has no kickoff message\"))?;\n    Ok(&messages[start..])\n}\n\npub fn trailing_error(conversation: &Conversation) -> Option<MessageErrorKind> {\n    conversation.last().and_then(Message::error_kind)\n}\n\npub fn last_effective_role(messages: &[Message]) -> Result<EffectiveRole> {\n    messages\n        .last()\n        .map(effective_role)\n        .ok_or_else(|| anyhow!(\"cannot determine the role of an empty conversation\"))\n}\n\npub fn assistant_turn_count(messages: &[Message]) -> u32 {\n    let mut turns = 0;\n    let mut in_assistant_block = false;\n    for message in messages.iter().rev() {","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/crates/goose-agent/src/operation.rs#L11-L47","documentation":"anyhow error from messages_since_kickoff (crates/goose-agent/src/operation.rs) when the conversation contains no message with role User that is user-visible and not a tool response (searched from the end via rposition). The state machine defines the turn as everything since that kickoff user message, so a conversation without one has no definable turn.","triggerScenarios":"Running a state-machine step on a conversation whose messages are only assistant messages and/or user-role tool responses (all tool responses or all filtered as hidden), or an effectively empty message list.","commonSituations":"Resuming a session that only recorded tool traffic; replaying a machine-generated conversation with no human kickoff; a truncation/context-management step removed the kickoff user message.","solutions":["Resume from a session that began with a normal user message, or start a new session","If context management trimmed the kickoff, keep at least one visible user message in the retained window","When building conversations programmatically, insert the kickoff user message before stepping the machine","If this reproduces on a normal interactive session, file it — the loader may be dropping user messages"],"exampleFix":"// before — stepping a conversation built only from tool responses\nmachine.step(&session, &emit).await?;\n\n// after — ensure a visible, non-tool-response user message exists first\nif messages_since_kickoff(conversation).is_err() {\n    conversation.push(Message::user(\"kickoff\"));\n}\nmachine.step(&session, &emit).await?;","handlingStrategy":"validation","validationCode":"let ok = conversation.messages().iter().any(|m| m.role == rmcp::model::Role::User && m.is_user_visible() && !m.is_tool_response());\nif !ok { /* push kickoff or refuse to step */ }","typeGuard":"fn has_kickoff(conversation: &Conversation) -> bool {\n    conversation.messages().iter().any(|m| {\n        m.role == rmcp::model::Role::User && m.is_user_visible() && !m.is_tool_response()\n    })\n}","tryCatchPattern":"match messages_since_kickoff(conversation) {\n    Ok(slice) => step_with(slice),\n    Err(_) => { conversation.push(Message::user(\"kickoff\")); retry_or_report() }\n}","preventionTips":["Always create sessions with an initial user message","Ensure context trimming keeps the kickoff message","Do not hand the machine conversations built solely from tool responses"],"tags":["rust","state-machine","conversation","goose-agent"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}