Skip to main content

Session Management

Overview

SDAC uses server-side session management to persist conversation history between the AI Widget and the Coordinator Release Agent. This approach provides:

  • Context persistence — Conversations survive page refreshes and browser restarts
  • Token tracking — Model usage logged per turn for cost analysis
  • Automatic expiration — 90-day retention with automatic cleanup
  • Cross-session context — Agent can reference previous interactions about the same report

Architecture

API Endpoint

POST /api/ingestion/sdac/chat

Main same-origin widget proxy endpoint for AI Widget conversations. It forwards to the source-backed runtime route POST /sdac/chat, handles session management automatically, and streams responses via Server-Sent Events (SSE).

Request:

{
"reportId": "8201EDC2-2EDE-4CA1-AF44-D0F5AA185CDB",
"message": "What is the fringe variance for this report?",
"userId": "auditor@state.gov",
"sessionId": "browser-session-abc123",
"conversationId": "browser-session-abc123" // Optional: omit for new conversation
}
FieldTypeRequiredDescription
reportIdUUIDYesCost report being discussed
messagestringYesUser's message
userIdstringYesUser identifier from auth
sessionIdstringYesBrowser session ID
conversationIdstringNoExisting conversation to continue

Response: Server-Sent Events stream

event: metadata
data: {"conversationId":"abc123","turnNumber":1,"isNewConversation":true,"conversationExpired":false}

event: delta
data: {"content":"Based on my analysis"}

event: delta
data: {"content":" of the fringe benefits..."}

event: usage
data: {"promptTokens":245,"completionTokens":89}

event: done
data: {"success":true}

SSE Event Types

EventDescription
metadataSent first. Contains session info (conversationId, turnNumber, isNew)
deltaStreamed content chunks from the agent
usageToken consumption (prompt + completion)
doneStream complete
errorError occurred during processing

Conversation Flow

New Conversation

  1. Frontend sends request without conversationId
  2. Server creates new conversation using sessionId as the key
  3. Inserts system turn (turn 0) to initialize
  4. Logs user message (turn 1)
  5. Streams agent response
  6. Logs agent response (turn 2)
  7. Returns conversationId in metadata event

Continuing Conversation

  1. Frontend sends request with conversationId
  2. Server validates conversation exists and is not expired
  3. Loads conversation history for context
  4. Appends history to agent messages
  5. Logs user message (turn N)
  6. Streams agent response with full context
  7. Logs agent response (turn N+1)

Expired Conversation

If a conversation has expired (>90 days old):

  1. Server detects expiration
  2. Creates new conversation automatically
  3. Sets conversationExpired: true in metadata
  4. Frontend can inform user that context was reset

Configuration

Environment Variables

VariableDefaultDescription
SDAC_CONVERSATION_MAX_TURNS10Max history turns to load for context

Retention Policy

Conversations expire after 90 days by default. The ExpiresAtUtc column is set on insert:

DATEADD(DAY, 90, SYSUTCDATETIME())

Expired rows are excluded from queries but not automatically deleted. Implement a cleanup job for production:

DELETE FROM SDAC.fact_ConversationHistory
WHERE ExpiresAtUtc < SYSUTCDATETIME();

Frontend Integration

Basic Integration

async function sendMessage(message: string) {
const response = await fetch('/api/ingestion/sdac/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reportId: currentReportId,
message,
userId: currentUser.id,
sessionId: getSessionId(),
conversationId: currentConversationId || undefined,
}),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() || '';

for (const event of events) {
const lines = event.split('\n');
let eventType = '';
let eventData = '';

for (const line of lines) {
if (line.startsWith('event:')) eventType = line.slice(6).trim();
if (line.startsWith('data:')) eventData = line.slice(5).trim();
}

if (!eventData) continue;
const json = JSON.parse(eventData);

switch (eventType) {
case 'metadata':
currentConversationId = json.conversationId;
break;
case 'delta':
appendToResponse(json.content);
break;
case 'done':
finalizeResponse();
break;
}
}
}
}

Session ID Management

Generate a persistent session ID for the browser:

function getSessionId(): string {
let sessionId = sessionStorage.getItem('sdac-session-id');
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem('sdac-session-id', sessionId);
}
return sessionId;
}

Database Schema

See Database Overview for the full fact_ConversationHistory schema.

Key columns for session management:

ColumnPurpose
SessionIdConversation key (matches browser session)
TurnNumberSequence within conversation (0=system, odd=user, even=assistant)
Rolesystem, user, or assistant
MessageContentFull message text
ExpiresAtUtcWhen conversation expires

Code Reference

FilePurpose
sdac-chat.routes.tsSSE endpoint and streaming logic
conversation-manager.tsSession orchestration
session-db.tsSQL operations
index.tsSession tools and exports

Troubleshooting

"Conversation not found"

The conversationId doesn't exist or has expired. The server will automatically create a new conversation.

Missing context in responses

Check SDAC_CONVERSATION_MAX_TURNS - if set too low, the agent won't see enough history.

Token columns showing NULL

Verify that buildSdacCoordinatorReleaseAgentFresh() is being used (not the cached getter) to get accurate model deployment info for logging.