Skip to main content

Overview

Handler’s session management system persists conversation state across CLI invocations and TUI sessions. Sessions store:
  • context_id: Conversation context for continuity
  • task_id: Current or most recent task
  • credentials: Authentication tokens (bearer, API key)
Sessions are stored in ~/.handler/sessions.json and automatically loaded when interacting with known agents.

Session Storage Location

Sessions are stored in your home directory:
~/.handler/sessions.json
The directory is created automatically on first use:
session.py:17-18
DEFAULT_SESSION_DIRECTORY = Path.home() / ".handler"
SESSION_FILENAME = "sessions.json"

Session Data Structure

Each agent URL maps to a session:
@dataclass
class AgentSession:
    """Session state for a single agent."""
    
    agent_url: str
    context_id: str | None = None
    task_id: str | None = None
    credentials: AuthCredentials | None = None
    
    def update(
        self,
        context_id: str | None = None,
        task_id: str | None = None,
        credentials: AuthCredentials | None = None,
    ) -> None:
        """Update session with new values (only if provided)."""
        if context_id is not None:
            self.context_id = context_id
        if task_id is not None:
            self.task_id = task_id
        if credentials is not None:
            self.credentials = credentials
    
    def clear_credentials(self) -> None:
        """Clear stored credentials."""
        self.credentials = None

Example Session File

sessions.json
{
  "http://localhost:8000": {
    "context_id": "ctx-abc123-def456",
    "task_id": "task-xyz789",
    "credentials": {
      "auth_type": "bearer",
      "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "header_name": null
    }
  },
  "https://api.example.com": {
    "context_id": "ctx-conversation-42",
    "task_id": "task-processing-99",
    "credentials": {
      "auth_type": "api_key",
      "value": "sk-1234567890abcdef",
      "header_name": "X-API-Key"
    }
  }
}

Automatic Session Management

Handler automatically saves session state after each interaction:
def update(
    self,
    agent_url: str,
    context_id: str | None = None,
    task_id: str | None = None,
    credentials: AuthCredentials | None = None,
) -> AgentSession:
    """Update session for an agent and save."""
    agent_session = self.get(agent_url)
    agent_session.update(context_id, task_id, credentials)
    self.save()  # Automatically persists to disk
    logger.debug(
        "Updated session for %s: context_id=%s, task_id=%s, has_credentials=%s",
        agent_url,
        context_id,
        task_id,
        credentials is not None,
    )
    return agent_session
In the MCP server, sessions update after each message:
mcp/server.py:203-204
result = await service.send(message, context_id, task_id)
update_session(agent_url, result.context_id, result.task_id)

Using Sessions with the CLI

Sessions are automatically loaded and used:
# First interaction - creates new session
handler message send \
  --url http://localhost:8000 \
  "What can you help me with?"
# Session saved: context_id=ctx-123, task_id=task-456

# Second interaction - uses saved session
handler message send \
  --url http://localhost:8000 \
  "Tell me more about the first feature"
# Uses context_id=ctx-123 for continuity
The CLI loads sessions on startup:
session.py:200-207
def get_session_store() -> SessionStore:
    """Get the global session store (singleton)."""
    global _global_session_store
    if _global_session_store is None:
        _global_session_store = SessionStore()
        _global_session_store.load()  # Load from disk
        logger.debug("Initialized global session store")
    return _global_session_store

Managing Sessions

1

List all sessions

View all saved sessions:
handler session list
Output:
Saved Sessions (3)

http://localhost:8000
  Context ID: ctx-abc123
  Task ID: task-xyz789

https://api.example.com
  Context ID: ctx-def456
  Task ID: task-uvw321

http://agent.local:3000
  Context ID: ctx-ghi789
Implementation:
cli/session.py:17-35
@session.command("list")
def session_list() -> None:
    """List all saved sessions."""
    output = Output()
    store = get_session_store()
    sessions = store.list_all()
    
    if not sessions:
        output.dim("No saved sessions")
        return
    
    output.header(f"Saved Sessions ({len(sessions)})")
    for s in sessions:
        output.blank()
        output.subheader(s.agent_url)
        if s.context_id:
            output.field("Context ID", s.context_id, dim_value=True)
        if s.task_id:
            output.field("Task ID", s.task_id, dim_value=True)
2

View specific session

Show details for a single agent:
handler session show http://localhost:8000
Output:
Session for http://localhost:8000
Context ID: ctx-abc123-def456
Task ID: task-xyz789-uvw321
3

Clear sessions

Clear a specific agent’s session:
handler session clear http://localhost:8000
Or clear all sessions:
handler session clear --all
Implementation:
session.py:175-190
def clear(self, agent_url: str | None = None) -> None:
    """Clear session(s).
    
    Args:
        agent_url: If provided, clear only that agent's session.
                  Otherwise, clear all sessions.
    """
    if agent_url:
        if agent_url in self.sessions:
            del self.sessions[agent_url]
            logger.info("Cleared session for %s", agent_url)
    else:
        session_count = len(self.sessions)
        self.sessions.clear()
        logger.info("Cleared all %d sessions", session_count)
    self.save()

Session Persistence

Sessions are loaded on demand and saved after modifications:
def load(self) -> None:
    """Load sessions from disk."""
    if not self.session_file_path.exists():
        logger.debug("No session file found at %s", self.session_file_path)
        return
    
    try:
        with open(self.session_file_path) as session_file:
            session_data = json.load(session_file)
        
        for agent_url, agent_session_data in session_data.items():
            credentials = None
            if cred_data := agent_session_data.get("credentials"):
                credentials = AuthCredentials.from_dict(cred_data)
            
            self.sessions[agent_url] = AgentSession(
                agent_url=agent_url,
                context_id=agent_session_data.get("context_id"),
                task_id=agent_session_data.get("task_id"),
                credentials=credentials,
            )
        
        logger.debug(
            "Loaded %d sessions from %s",
            len(self.sessions),
            self.session_file_path,
        )
    
    except json.JSONDecodeError as error:
        logger.warning("Failed to parse session file: %s", error)
    except OSError as error:
        logger.warning("Failed to read session file: %s", error)

Context Continuity

Context IDs enable multi-turn conversations with agents:
# First message: creates new context
result1 = await service.send("What's the weather?")
# context_id: "ctx-abc123"

# Second message: uses same context
result2 = await service.send("How about tomorrow?", context_id=result1.context_id)
# Agent has context from first message
The TUI manages context automatically:
app.py:162-163
self.current_agent_url = agent_url
self.current_context_id = str(uuid.uuid4())  # New context per connection
app.py:218-224
send_result = await self._agent_service.send(
    message_text,
    context_id=self.current_context_id,  # Reuse context
)

if send_result.context_id:
    self.current_context_id = send_result.context_id  # Update if changed

Using Sessions with MCP

The MCP server provides tools for session management:
@mcp.tool()
async def list_sessions() -> dict:
    """List all saved sessions.
    
    Returns:
        count: Number of saved sessions
        sessions: List with agent_url, context_id, task_id, has_credentials
    """
    store = get_session_store()
    sessions = store.list_all()
    
    return {
        "count": len(sessions),
        "sessions": [
            {
                "agent_url": s.agent_url,
                "context_id": s.context_id,
                "task_id": s.task_id,
                "has_credentials": s.credentials is not None,
            }
            for s in sessions
        ],
    }

Credentials in Sessions

Sessions can store authentication credentials for automatic reuse:
1

Save credentials

handler auth set \
  --url http://localhost:8000 \
  --bearer-token "your-token"
session.py:150-160
def set_credentials(
    self,
    agent_url: str,
    credentials: AuthCredentials,
) -> AgentSession:
    """Set credentials for an agent."""
    agent_session = self.get(agent_url)
    agent_session.credentials = credentials
    self.save()
    logger.info("Set credentials for %s", agent_url)
    return agent_session
2

Auto-load credentials

Credentials are automatically applied:
service.py:236-242
def __init__(
    self,
    http_client: httpx.AsyncClient,
    agent_url: str,
    # ...
    credentials: AuthCredentials | None = None,
) -> None:
    # ...
    if credentials:
        self.set_credentials(credentials)
3

Clear credentials

Remove only credentials (keep context/task):
handler auth clear --url http://localhost:8000
Credentials in sessions.json are stored in plain text. See Authentication Security for best practices.

Session File Permissions

Protect your session file:
chmod 600 ~/.handler/sessions.json
chmod 700 ~/.handler
Add to .gitignore if working in a repo:
.gitignore
.handler/
sessions.json

Troubleshooting

Check if the session file exists and is valid JSON:
cat ~/.handler/sessions.json | jq
If corrupt, backup and recreate:
mv ~/.handler/sessions.json ~/.handler/sessions.json.bak
handler session list
Verify session is being saved:
handler message send --url http://localhost:8000 "Test"
handler session show http://localhost:8000
Check for write permissions:
ls -la ~/.handler/
List sessions to verify credentials are saved:
handler session list
Check the session file:
jq '."http://localhost:8000".credentials' ~/.handler/sessions.json

Best Practices

One Session Per Agent

Each agent URL gets its own session. Use different URLs (ports, paths) for different agent instances.

Clear Stale Sessions

Periodically clear old sessions:
handler session clear --all

Explicit Context

For critical workflows, pass --context-id explicitly rather than relying on session state.

Backup Sessions

Before major changes:
cp ~/.handler/sessions.json ~/.handler/sessions.json.bak

Next Steps