charmbracelet/crush · error

session_id is required

Error message

session_id is required

What it means

The write tool requires a session ID embedded in the tool-call context to attribute file writes (and their history) to a session. GetSessionFromContext returned an empty string, meaning the context was built without a session. This is an internal invariant violation rather than a user-facing input problem.

Source

Thrown at internal/agent/tools/write.go:63

func NewWriteTool(
	lspManager *lsp.Manager,
	permissions permission.Service,
	files history.Service,
	filetracker filetracker.Service,
	workingDir string,
) fantasy.AgentTool {
	return fantasy.NewAgentTool(
		WriteToolName,
		writeDescription,
		func(ctx context.Context, params WriteParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
			if params.FilePath == "" {
				return fantasy.NewTextErrorResponse("file_path is required"), nil
			}

			sessionID := GetSessionFromContext(ctx)
			if sessionID == "" {
				return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
			}

			filePath := filepathext.SmartJoin(workingDir, params.FilePath)

			fileInfo, err := os.Stat(filePath)
			if err == nil {
				if fileInfo.IsDir() {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
				}

				modTime := fileInfo.ModTime().Truncate(time.Second)
				lastRead := filetracker.LastReadTime(ctx, sessionID, filePath)
				if modTime.After(lastRead) {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
						filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
				}

				oldContent, readErr := os.ReadFile(filePath)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Populate the session ID in the context before invoking the tool, using the same helper the agent pipeline uses (WithSession/GetSessionFromContext pair).
  2. If driving tools manually, wrap the context with the session ID from the created session before each tool call.
  3. If you own the calling code, assert sessionID != "" before dispatching tool calls to fail fast with a clearer message.

Example fix

// before
tool.Run(ctx, params)

// after
ctx = agent.WithSession(ctx, sess.ID)
tool.Run(ctx, params)
Defensive patterns

Strategy: validation

Validate before calling

if agent.GetSessionFromContext(ctx) == "" {
    return fmt.Errorf("tool call aborted: no session in context")
}
_ = tool.Run(ctx, params)

Type guard

func hasSession(ctx context.Context) bool {
    return agent.GetSessionFromContext(ctx) != ""
}

Try / catch

if _, err := tool.Run(ctx, params); err != nil {
    if err.Error() == "session_id is required" {
        return fmt.Errorf("tool invoked without session context; wrap ctx with the session helper: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling the write tool's Run with a context.Context that was not populated via the session-context helper (e.g. invoking the tool directly in tests, custom harnesses, or code paths that construct tool calls manually without the session value).

Common situations: Unit/integration tests that invoke tools directly; custom agent runners or MCP-style drivers that skip the coordinator's context setup; code refactors that pass a bare ctx instead of the one produced by the agent pipeline.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/1a9c4ffb2961cb4e. Report an issue: GitHub.