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
}
| Field | Type | Required | Description |
|---|---|---|---|
reportId | UUID | Yes | Cost report being discussed |
message | string | Yes | User's message |
userId | string | Yes | User identifier from auth |
sessionId | string | Yes | Browser session ID |
conversationId | string | No | Existing 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
| Event | Description |
|---|---|
metadata | Sent first. Contains session info (conversationId, turnNumber, isNew) |
delta | Streamed content chunks from the agent |
usage | Token consumption (prompt + completion) |
done | Stream complete |
error | Error occurred during processing |
Conversation Flow
New Conversation
- Frontend sends request without
conversationId - Server creates new conversation using
sessionIdas the key - Inserts system turn (turn 0) to initialize
- Logs user message (turn 1)
- Streams agent response
- Logs agent response (turn 2)
- Returns
conversationIdin metadata event
Continuing Conversation
- Frontend sends request with
conversationId - Server validates conversation exists and is not expired
- Loads conversation history for context
- Appends history to agent messages
- Logs user message (turn N)
- Streams agent response with full context
- Logs agent response (turn N+1)
Expired Conversation
If a conversation has expired (>90 days old):
- Server detects expiration
- Creates new conversation automatically
- Sets
conversationExpired: truein metadata - Frontend can inform user that context was reset
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
SDAC_CONVERSATION_MAX_TURNS | 10 | Max 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:
| Column | Purpose |
|---|---|
SessionId | Conversation key (matches browser session) |
TurnNumber | Sequence within conversation (0=system, odd=user, even=assistant) |
Role | system, user, or assistant |
MessageContent | Full message text |
ExpiresAtUtc | When conversation expires |
Code Reference
| File | Purpose |
|---|---|
sdac-chat.routes.ts | SSE endpoint and streaming logic |
conversation-manager.ts | Session orchestration |
session-db.ts | SQL operations |
index.ts | Session 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.
Related Documentation
- SDAC Agents — Agent that powers the AI Widget
- SDAC Database — Full schema reference
- SDAC Tools — Session management tools