Series 6 — Part 7 of 10
An LLM without conversation context produces generic responses. the WhatsApp AI agent stores the last N messages per user and injects them into every prompt. This article covers per-user session management, context injection, session expiry, and multi-device handling.
Session Schema
CREATE TABLE wa_sessions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
team_id INT UNSIGNED NOT NULL,
wa_contact_id BIGINT UNSIGNED NOT NULL,
session_key VARCHAR(40) NOT NULL UNIQUE,
last_active_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
context_turns TINYINT UNSIGNED NOT NULL DEFAULT 0,
INDEX idx_contact_session (wa_contact_id, expires_at)
);
CREATE TABLE wa_session_messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
session_id BIGINT UNSIGNED NOT NULL,
role ENUM('user','assistant') NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES wa_sessions(id) ON DELETE CASCADE
);
Context Injection
class SessionManager
{
private const SESSION_TTL_HOURS = 4;
private const MAX_CONTEXT_TURNS = 10;
public function getOrCreateSession(int $contactId, int $teamId, PDO $pdo): array
{
$stmt = $pdo->prepare(
'SELECT * FROM wa_sessions WHERE wa_contact_id = ? AND expires_at > NOW()'
);
$stmt->execute([$contactId]);
$session = $stmt->fetch();
if (!$session) {
$pdo->prepare(
'INSERT INTO wa_sessions (team_id, wa_contact_id, session_key, expires_at)
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL ? HOUR))'
)->execute([$teamId, $contactId, bin2hex(random_bytes(20)), self::SESSION_TTL_HOURS]);
$session = $pdo->query("SELECT * FROM wa_sessions WHERE id = LAST_INSERT_ID()")->fetch();
}
return $session;
}
public function getContextMessages(int $sessionId, PDO $pdo): array
{
$stmt = $pdo->prepare(
'SELECT role, content FROM wa_session_messages
WHERE session_id = ?
ORDER BY created_at DESC LIMIT ' . (self::MAX_CONTEXT_TURNS * 2)
);
$stmt->execute([$sessionId]);
$rows = $stmt->fetchAll();
return array_reverse($rows); // Restore chronological order
}
public function appendMessage(int $sessionId, string $role, string $content, PDO $pdo): void
{
$pdo->prepare(
'INSERT INTO wa_session_messages (session_id, role, content) VALUES (?,?,?)'
)->execute([$sessionId, $role, $content]);
// Keep only the last MAX_CONTEXT_TURNS * 2 messages
$pdo->prepare(
'DELETE FROM wa_session_messages WHERE session_id = ? AND id NOT IN (
SELECT id FROM (
SELECT id FROM wa_session_messages WHERE session_id = ?
ORDER BY created_at DESC LIMIT ' . (self::MAX_CONTEXT_TURNS * 2) . '
) t
)'
)->execute([$sessionId, $sessionId]);
}
}
What to Watch For
- Session expiry on logout — When a user explicitly says "goodbye" or "end session", expire the session immediately rather than waiting for TTL.
- Multi-device scope — A WhatsApp number can be used on multiple devices. Sessions are per wa_contact (phone number), not per device. This is correct — it prevents context fragmentation.
- Context window budget — The session context plus the system prompt plus the current message must fit within the LLM's context window. Always measure the combined token count before the LLM call.