Skip to main content

What are Sessions?

In Uzi, a session is a persistent Tmux environment where an AI agent operates. Each session provides:
  • Isolation: Dedicated workspace for one agent
  • Persistence: Continues running even if you disconnect
  • Multi-window support: Separate panes for agent and development server
  • Easy access: Attach to any session to observe or interact
Tmux (Terminal Multiplexer) allows programs to run in the background and persist across terminal disconnections. Uzi leverages this to keep agents running independently.

Session Structure

Each Uzi session follows a standardized structure:
Session: agent-myproject-a1b2c3d-sarah
├── Window 0: agent
│   └── AI agent runs here, executing commands and making changes
└── Window 1: uzi-dev (optional)
    └── Development server (if configured)

Session Creation

When you run uzi prompt, Uzi creates a new Tmux session:
// From prompt.go:215-220
cmd = fmt.Sprintf("tmux new-session -d -s %s -c %s", sessionName, worktreePath)
cmdExec = exec.CommandContext(ctx, "sh", "-c", cmd)
if err := cmdExec.Run(); err != nil {
    log.Error("Error creating tmux session", "command", cmd, "error", err)
}
This creates a detached session (-d) with the specified name and working directory.

Session Naming Convention

Session names follow a strict format that encodes important metadata:
agent-{projectDir}-{gitHash}-{agentName}

Components

ComponentDescriptionExample
agentFixed prefixagent
projectDirRepository namemyapp
gitHashShort Git commit hasha1b2c3d
agentNameRandom agent namesarah
Example: agent-myapp-a1b2c3d-sarah

Name Generation

The session name is constructed from Git metadata:
// From prompt.go:166-188
// Get git hash
gitHashCmd := exec.CommandContext(ctx, "git", "rev-parse", "--short", "HEAD")
gitHashOutput, err := gitHashCmd.Output()
gitHash := strings.TrimSpace(string(gitHashOutput))

// Get repository name
gitRemoteCmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin")
gitRemoteOutput, err := gitRemoteCmd.Output()
remoteURL := strings.TrimSpace(string(gitRemoteOutput))
repoName := filepath.Base(remoteURL)
projectDir := strings.TrimSuffix(repoName, ".git")

// Create session name
sessionName := fmt.Sprintf("agent-%s-%s-%s", projectDir, gitHash, randomAgentName)
The session name uniquely identifies the agent and its context. This naming scheme prevents collisions when running multiple Uzi agents across different projects.

Window 0: Agent Pane

The first window (named “agent”) is where the AI agent operates.

Window Renaming

After creating the session, Uzi renames the default window:
// From prompt.go:222-228
renameCmd := fmt.Sprintf("tmux rename-window -t %s:0 agent", sessionName)
renameExec := exec.CommandContext(ctx, "sh", "-c", renameCmd)
if err := renameExec.Run(); err != nil {
    log.Error("Error renaming tmux window", "command", renameCmd, "error", err)
}

Sending Commands

Uzi sends the agent command to this window:
// From prompt.go:305-310
tmuxCmd := fmt.Sprintf("tmux send-keys -t %s:agent '%s \"%s\"' C-m", sessionName, commandToUse)
tmuxCmdExec := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf(tmuxCmd, promptText))
if err := tmuxCmdExec.Run(); err != nil {
    log.Error("Error sending keys to tmux", "command", tmuxCmd, "error", err)
}
This sends: {commandToUse} "{promptText}" followed by Enter (C-m).
You can attach to the agent pane to watch it work: tmux attach -t agent-myapp-a1b2c3d-sarah:agent

Window 1: Development Server (uzi-dev)

If you’ve configured a development command, Uzi creates a second window for the dev server.

Configuration

Development server settings are configured in uzi.yaml in your project root:
devCommand: npm run dev -- --port $PORT
portRange: 3000-4000
  • devCommand: Command to start your dev server (use $PORT placeholder)
  • portRange: Range of ports to allocate (format: start-end)

Port Allocation

Uzi automatically finds an available port in the configured range:
// From prompt.go:271-275
selectedPort, err = findAvailablePort(startPort, endPort, assignedPorts)
if err != nil {
    log.Error("Error finding available port", "error", err)
}
The findAvailablePort function:
// From prompt.go:89-109
func findAvailablePort(startPort, endPort int, assignedPorts []int) (int, error) {
    for port := startPort; port <= endPort; port++ {
        // Check if already assigned
        alreadyAssigned := false
        for _, assignedPort := range assignedPorts {
            if port == assignedPort {
                alreadyAssigned = true
                break
            }
        }
        if alreadyAssigned {
            continue
        }
        
        // Check if actually available
        if isPortAvailable(port) {
            return port, nil
        }
    }
    return 0, fmt.Errorf("no available ports in range %d-%d", startPort, endPort)
}

Window Creation

The uzi-dev window is created in the same session:
// From prompt.go:280-286
newWindowCmd := fmt.Sprintf("tmux new-window -t %s -n uzi-dev -c %s", sessionName, worktreePath)
newWindowExec := exec.CommandContext(ctx, "sh", "-c", newWindowCmd)
if err := newWindowExec.Run(); err != nil {
    log.Error("Error creating new tmux window for dev server", "command", newWindowCmd, "error", err)
}

Starting the Dev Server

The dev command is sent to the uzi-dev window:
// From prompt.go:277-293
devCmdTemplate := *cfg.DevCommand
devCmd := strings.Replace(devCmdTemplate, "$PORT", strconv.Itoa(selectedPort), 1)

sendDevCmd := fmt.Sprintf("tmux send-keys -t %s:uzi-dev '%s' C-m", sessionName, devCmd)
sendDevCmdExec := exec.CommandContext(ctx, "sh", "-c", sendDevCmd)
if err := sendDevCmdExec.Run(); err != nil {
    log.Error("Error sending dev command to tmux", "command", sendDevCmd, "error", err)
}
Example: If dev_command is npm run dev -- --port $PORT and selectedPort is 3001, this runs:
npm run dev -- --port 3001
The development server runs independently of the agent, allowing the AI to interact with a live preview of the application.

Session Monitoring

Listing Active Sessions

Use uzi ls to see all active sessions:
$ uzi ls
AGENT   MODEL   STATUS    DIFF        ADDR                    PROMPT
sarah   claude  running   +45/-12     http://localhost:3000   Implement auth
john    codex   ready     +23/-8      http://localhost:3001   Fix parser bug
The status is determined by inspecting the agent pane:
// From ls.go:94-104
func getAgentStatus(sessionName string) string {
    content, err := getPaneContent(sessionName)
    if err != nil {
        return "unknown"
    }
    
    if strings.Contains(content, "esc to interrupt") || strings.Contains(content, "Thinking") {
        return "running"
    }
    return "ready"
}

Pane Content Capture

Uzi captures the visible content of the agent pane:
// From ls.go:85-92
func getPaneContent(sessionName string) (string, error) {
    cmd := exec.Command("tmux", "capture-pane", "-t", sessionName+":agent", "-p")
    output, err := cmd.Output()
    if err != nil {
        return "", err
    }
    return string(output), nil
}

Watch Mode

Monitor sessions in real-time:
uzi ls -w
This refreshes the display every second:
// From ls.go:223-262
if *watchMode {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            clearScreen()
            activeSessions, err := stateManager.GetActiveSessionsForRepo()
            // ... print sessions
        }
    }
}
Press Ctrl+C to exit watch mode.

Session State

Session metadata is stored in ~/.local/share/uzi/state.json:
{
  "agent-myapp-a1b2c3d-sarah": {
    "git_repo": "[email protected]:user/myapp.git",
    "branch_from": "main",
    "branch_name": "sarah-myapp-a1b2c3d-1234",
    "prompt": "Implement authentication",
    "worktree_path": "/home/user/.local/share/uzi/worktrees/sarah-myapp-a1b2c3d-1234",
    "port": 3000,
    "model": "claude",
    "created_at": "2024-03-15T10:30:00Z",
    "updated_at": "2024-03-15T10:45:00Z"
  }
}

Active Session Detection

Uzi checks if a session is active using Tmux:
// From state.go:74-77
func (sm *StateManager) isActiveInTmux(sessionName string) bool {
    cmd := exec.Command("tmux", "has-session", "-t", sessionName)
    return cmd.Run() == nil
}

Repository Filtering

The GetActiveSessionsForRepo method filters sessions by Git repository:
// From state.go:79-106
func (sm *StateManager) GetActiveSessionsForRepo() ([]string, error) {
    // Load state
    states := make(map[string]AgentState)
    // ... load from state.json
    
    currentRepo := sm.getGitRepo()
    if currentRepo == "" {
        return []string{}, nil
    }
    
    var activeSessions []string
    for sessionName, state := range states {
        if state.GitRepo == currentRepo && sm.isActiveInTmux(sessionName) {
            activeSessions = append(activeSessions, sessionName)
        }
    }
    
    return activeSessions, nil
}
This ensures uzi ls only shows sessions for the current repository.

Interacting with Sessions

Attach to a Session

Connect to a session to observe or interact:
# Attach to the entire session
tmux attach -t agent-myapp-a1b2c3d-sarah

# Attach to specific window
tmux attach -t agent-myapp-a1b2c3d-sarah:agent
tmux attach -t agent-myapp-a1b2c3d-sarah:uzi-dev

Detach from a Session

While attached, press Ctrl+B then D to detach without killing the session.

Send Keys to a Session

Send commands programmatically:
tmux send-keys -t agent-myapp-a1b2c3d-sarah:agent "git status" C-m

Session Cleanup

When you kill an agent, Uzi removes its session:
// From kill.go:33-43
checkSession := exec.CommandContext(ctx, "tmux", "has-session", "-t", sessionName)
if err := checkSession.Run(); err == nil {
    // Session exists, kill it
    killCmd := exec.CommandContext(ctx, "tmux", "kill-session", "-t", sessionName)
    if err := killCmd.Run(); err != nil {
        log.Error("Error killing tmux session", "session", sessionName, "error", err)
    }
}
This also removes:
  • The Git worktree
  • The Git branch
  • The state entry
  • The worktree metadata directory
Killing a session is permanent. Make sure to checkpoint any work you want to keep before running uzi kill.

Best Practices

Use Descriptive Configuration

Configure your dev server with the $PORT placeholder:
{
  "dev_command": "npm run dev -- --port $PORT",
  "port_range": "3000-4000"
}

Monitor Sessions Regularly

Check session status frequently:
# Quick check
uzi ls

# Real-time monitoring
uzi ls -w

Attach to Observe Agent Work

When debugging or learning, attach to sessions:
tmux attach -t agent-myapp-a1b2c3d-sarah:agent

Clean Up Completed Sessions

Remove finished agents to free resources:
uzi kill sarah

# Or remove all
uzi kill all

Troubleshooting

If uzi ls doesn’t show your session:
  • Check if the session is still active: tmux ls
  • Verify you’re in the correct Git repository
  • Inspect ~/.local/share/uzi/state.json for the session entry
If the dev server fails to start:
  • Increase your port_range in config
  • Kill unused agents: uzi kill all
  • Manually check for processes using ports: lsof -i :3000
If tmux attach fails:
  • Ensure Tmux is installed: which tmux
  • Check the session name is correct: tmux ls
  • Try tmux attach -t {sessionName} without the window suffix

Next Steps

Worktrees

Learn about Git worktree isolation

Kill Command

Master session cleanup and deletion

Build docs developers (and LLMs) love