Sessions
A session is a conversation thread between an app and an agent. Relay tracks sessions to enable:
- Conversation continuity (agent remembers context)
- Event deduplication
- Timeouts and expiry
- Multi-turn conversations
What Is a Session?
A session is identified by a session key, which has three components:
session_key = "relay:<agent_id>:<app_id>:<thread_id>"
Example:
relay:athena:portal:task-123
This means: "The conversation between Athena and Portal for task-123."
Session Components
| Component | Example | Notes |
|---|---|---|
| Prefix | relay: | Always "relay:" |
| Agent ID | athena | Which agent |
| App ID | portal | Which app (derived from token) |
| Thread ID | task-123 | The topic/context (you define this) |
What Is thread_id?
The thread_id is a string you provide when sending an event. It identifies the conversation topic within your app.
Examples:
Portal (task-based):
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": { ... }
}
Portal (comment-thread based):
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123:comment-456",
"payload": { ... }
}
Flow (project-task based):
{
"type": "event",
"agent_id": "klyve",
"thread_id": "project-789:task-456",
"payload": { ... }
}
Academy (course-module based):
{
"type": "event",
"agent_id": "athena",
"thread_id": "course-101:module-functions",
"payload": { ... }
}
Use a format that makes sense for your app's domain.
Sessions Enable Continuity
Without sessions, each event is independent:
Event 1: "What is a REST API?"
Reply: "REST is..."
Event 2: "How do I implement pagination?"
Reply: "Pagination is..." (agent doesn't remember REST context)
With sessions, the agent can maintain context:
Event 1: "What is a REST API?"
(session: relay:athena:academy:course-101:apis)
Reply: "REST is..."
Event 2: "How do I implement pagination?"
(same session_key)
Reply: "In the context of REST APIs, pagination is..." (agent remembers!)
The agent receives the session_key in each event and can use it to fetch conversation history or maintain state.
Session Continuity Across Days
Sessions don't expire just because time has passed. You can:
Day 1:
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": { "question": "What's the architecture?" }
}
Agent replies. Session is stored.
Day 3:
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": { "question": "Tell me more about the database layer" }
}
Same session_key. Agent can access the previous conversation and maintain context.
This is powerful for:
- Long-running projects (team comes back to task days later)
- Reviews that span days
- Learning paths where students revisit topics
- Bug investigations that take time
Session State
Each session has:
| Field | Type | Description |
|---|---|---|
session_key | string | Unique identifier |
agent_id | string | The agent |
app_id | string | The app |
thread_id | string | The topic |
created_at | timestamp | When session started |
last_activity_at | timestamp | When last event was received |
expires_at | timestamp | When session will be deleted |
message_count | integer | Number of events processed |
Session TTL (Time-to-Live)
Sessions have a TTL (time-to-live). After the TTL expires, the session is cleaned up.
Default TTL: 30 days
When you send an event:
- If session doesn't exist → create it, set expires_at to now + 30 days
- If session exists → update expires_at to now + 30 days
This means: sessions expire if there's no activity for 30 days.
Example:
Created: 2026-04-01
Last activity: 2026-04-10
Expires at: 2026-05-10 (30 days after last activity)
If no activity until 2026-05-10 → session deleted
But if event comes on 2026-05-08:
Expires at resets to: 2026-06-07
The Relay vs. OpenClaw Model
In Relay, you provide the thread_id and Relay generates the session_key:
You send: thread_id = "task-123"
Relay: session_key = "relay:athena:portal:task-123"
Agent gets: session_key in the event
In OpenClaw (a different system), the session key format is different:
OpenClaw: session_key = "session:1a2b3c4d5e"
If you're integrating an agent that expects OpenClaw-style session keys, you need to translate. The OpenClaw Relay plugin handles this automatically.
Multi-Turn Conversations
Sessions are designed for multi-turn conversations:
Turn 1 (User asks a question):
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": {
"type": "question",
"text": "What are the top 3 priorities?",
"messages": [] // First message in conversation
}
}
Athena replies: "1. Performance 2. Security 3. Scalability"
Turn 2 (User asks follow-up):
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": {
"type": "question",
"text": "How do we improve performance?",
"messages": [
{
"role": "user",
"text": "What are the top 3 priorities?"
},
{
"role": "assistant",
"text": "1. Performance 2. Security 3. Scalability"
}
]
}
}
Same session_key. Agent sees the conversation history in the payload and can respond contextually.
Session Keys in Events vs. Replies
When you send an event, you provide the thread_id:
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": { ... }
}
When Relay accepts, it returns the session_key:
{
"type": "accepted",
"event_id": "evt_k9p2m",
"session_key": "relay:athena:portal:task-123",
"status": "accepted"
}
When the agent replies, it echoes back the session_key (because it was in the event it received):
{
"type": "reply",
"event_id": "evt_k9p2m",
"content": "...",
"session_key": "relay:athena:portal:task-123",
"payload": { ... }
}
When your app receives the reply, you get both event_id and session_key to correlate with your records.
Best Practices for thread_id
1. Make It Unique Per Conversation
Good: "task-123", "task-123:comment-456", "course-101:module-5"
Bad: "default", "1", "general"
2. Use Hierarchical Format (Optional)
Use colons or slashes to show hierarchy:
task-123
task-123:comment-456
task-123:comment-456:sub-thread-1
project-789:task-456
project-789:task-456:code-review
This makes debugging easier and helps with logging.
3. Include Domain Context
Good: "portal:task-123", "flow:project-789:task-456", "academy:course-101:module-5"
This makes logs more readable across multiple apps and agents.
4. Keep It Stable
Use IDs, not content. Instead of:
thread_id = "Summarize Q2 Roadmap Discussion"
Use:
thread_id = "task-123"
This way, if the task title changes, the session is still the same.
Example: Multi-Turn Task Review
Imagine a code review task in Flow:
Message 1:
# App sends
{
"type": "event",
"agent_id": "klyve",
"thread_id": "project-789:pr-123",
"payload": {
"type": "code_review_request",
"pr_title": "Add database indexing",
"pr_description": "Adds indexes to speed up queries",
"diff": "...",
"context": "Performance is a Q2 goal"
}
}
# Relay responds with
{
"type": "accepted",
"session_key": "relay:klyve:flow:project-789:pr-123"
}
# Klyve replies with
{
"type": "reply",
"content": "Looks good overall. A few suggestions: 1) Add composite index on user_id + timestamp. 2) Consider covering indexes for SELECT * queries.",
"session_key": "relay:klyve:flow:project-789:pr-123"
}
Message 2 (Developer asks follow-up):
# App sends
{
"type": "event",
"agent_id": "klyve",
"thread_id": "project-789:pr-123",
"payload": {
"type": "code_review_followup",
"question": "How do covering indexes compare to composite indexes?",
"previous_review": "Looks good overall. A few suggestions..."
}
}
# Klyve replies (with context from session)
{
"type": "reply",
"content": "Good question. In the context of your queries, covering indexes are better because they include all columns needed for SELECT *. Composite indexes are better when you need specific column ordering...",
"session_key": "relay:klyve:flow:project-789:pr-123"
}
Same session, continuity maintained.
Expiry and Cleanup
Sessions are cleaned up when they expire (30 days of inactivity by default).
Before cleanup:
- Session data is archived (for audit purposes)
- Old events and replies are logged permanently
- Session metrics are recorded
After cleanup:
- Session record is deleted
- New events on the same thread_id create a new session
- No conversation history is carried over
If you need to preserve conversation history beyond 30 days, your app should store it. Relay is designed for ephemeral conversations, not permanent archives.
Summary
Sessions enable:
✓ Conversation continuity (agent remembers context) ✓ Multi-turn exchanges (user asks, agent replies, user asks more) ✓ Long-running tasks (conversation stays open across days) ✓ Event deduplication (same event_id isn't processed twice) ✓ Audit trails (who said what, when)
Sessions are automatically managed by Relay. You just provide thread_id, and Relay handles the rest.
Next Steps
- Learn about Payload Design for conversation context
- Read Agents as Users to understand the broader model
- Check WebSocket Messages for session_key in event schemas