{"record":{"id":"51877833cac6d9c4","repo":"charmbracelet/crush","slug":"session-id-is-required-for-managing-todos","errorCode":null,"errorMessage":"session ID is required for managing todos","messagePattern":"session ID is required for managing todos","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/todos.go","lineNumber":43,"sourceCode":"}\n\ntype TodosResponseMetadata struct {\n\tIsNew         bool           `json:\"is_new\"`\n\tTodos         []session.Todo `json:\"todos\"`\n\tJustCompleted []string       `json:\"just_completed,omitempty\"`\n\tJustStarted   string         `json:\"just_started,omitempty\"`\n\tCompleted     int            `json:\"completed\"`\n\tTotal         int            `json:\"total\"`\n}\n\nfunc NewTodosTool(sessions session.Service) fantasy.AgentTool {\n\treturn fantasy.NewAgentTool(\n\t\tTodosToolName,\n\t\ttodosDescription,\n\t\tfunc(ctx context.Context, params TodosParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {\n\t\t\tsessionID := GetSessionFromContext(ctx)\n\t\t\tif sessionID == \"\" {\n\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"session ID is required for managing todos\")\n\t\t\t}\n\n\t\t\tcurrentSession, err := sessions.Get(ctx, sessionID)\n\t\t\tif err != nil {\n\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"failed to get session: %w\", err)\n\t\t\t}\n\n\t\t\tisNew := len(currentSession.Todos) == 0\n\t\t\toldStatusByContent := make(map[string]session.TodoStatus)\n\t\t\tfor _, todo := range currentSession.Todos {\n\t\t\t\toldStatusByContent[todo.Content] = todo.Status\n\t\t\t}\n\n\t\t\tfor _, item := range params.Todos {\n\t\t\t\tswitch item.Status {\n\t\t\t\tcase \"pending\", \"in_progress\", \"completed\":\n\t\t\t\tdefault:\n\t\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"invalid status %q for todo %q\", item.Status, item.Content)","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/todos.go#L25-L61","documentation":"The todos tool requires a session ID in its execution context to know which session's todo list to manage. GetSessionFromContext(ctx) returned an empty string, meaning the tool was invoked without the session-scoped context value that the agent runtime normally injects. This is an internal invariant error: the tool refuses to act rather than guess a session.","triggerScenarios":"Calling the todos tool with a context lacking the session ID context key — e.g. invoking the fantasy.AgentTool directly in tests or custom code instead of through the coordinator, or a code path that builds the tool call context without SetSessionInContext.","commonSituations":"Test harnesses or scripts that invoke the tool function directly; embedding the todos tool in a custom agent runner that doesn't replicate Crush's context wiring; regressions in agent plumbing after refactoring.","solutions":["Ensure the tool is invoked through the normal agent/coordinator path which sets the session ID in the context","In custom code, set the session ID in the context before calling the tool (SetSessionInContext or the equivalent context key)","If this occurs during normal usage, report it — it indicates a bug in Crush's tool context wiring"],"exampleFix":"// before\nresp, err := tool.Execute(ctx, call) // ctx has no session ID\n// after\nctx = tools.SetSessionInContext(ctx, sessionID)\nresp, err := tool.Execute(ctx, call)","handlingStrategy":"validation","validationCode":"if tools.GetSessionFromContext(ctx) == \"\" {\n    return errors.New(\"session ID must be set in context before invoking the todos tool\")\n}","typeGuard":"func hasSession(ctx context.Context) bool {\n    return tools.GetSessionFromContext(ctx) != \"\"\n}","tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"session ID is required\") {\n    // wire session into ctx and retry\n    return tool.Execute(tools.SetSessionInContext(ctx, sessionID), call)\n}","preventionTips":["Always invoke tools via the coordinator/agent runtime, never the raw tool function","In custom runners, replicate Crush's context wiring (session ID injection) before tool execution","Add a test asserting tool calls fail fast without a session to catch wiring regressions"],"tags":["go","context","session","tooling"],"backgroundTag":"missing-context-value","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}