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
Component Description Example agentFixed prefix agentprojectDirRepository name myappgitHashShort Git commit hash a1b2c3dagentNameRandom agent name sarah
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:
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