Skip to main content

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

ComponentExampleNotes
Prefixrelay:Always "relay:"
Agent IDathenaWhich agent
App IDportalWhich app (derived from token)
Thread IDtask-123The 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:

FieldTypeDescription
session_keystringUnique identifier
agent_idstringThe agent
app_idstringThe app
thread_idstringThe topic
created_attimestampWhen session started
last_activity_attimestampWhen last event was received
expires_attimestampWhen session will be deleted
message_countintegerNumber 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