Skip to content
Docs

Voice Session Persistence

Phone calls drop, WebSocket connections reset, and services restart during rolling deployments. Without persistence, users must start their conversation from scratch after any interruption. Voice session persistence saves session state — conversation context, form progress, collected fields — to an external store so sessions resume exactly where they left off. This is essential for multi-turn voice forms, long-running support calls, and any deployment that requires high availability. This guide covers implementing a persistence layer for Beluga AI voice sessions.

By persisting session-scoped state keyed by session ID, multi-turn voice flows such as forms and long conversations can resume where they left off. The persistence layer operates independently of the voice session itself, storing application-level state in an external store.

  • Go 1.23 or later
  • A persistence store (Redis, PostgreSQL, or similar)
  • Beluga AI voice session configured
Terminal window
go get github.com/lookatitude/beluga-ai

Keep session-scoped state separate from the voice session object:

package main
import (
"context"
"time"
)
type SessionState struct {
SessionID string `json:"session_id"`
FormAnswers map[string]string `json:"form_answers,omitempty"`
LastTurn string `json:"last_turn,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
type Store interface {
Get(ctx context.Context, sessionID string) (*SessionState, error)
Set(ctx context.Context, s *SessionState) error
Delete(ctx context.Context, sessionID string) error
}

After processing a user turn (in your form orchestrator or agent callback), update and persist state:

func saveState(ctx context.Context, sess session.VoiceSession, store Store, state *SessionState) error {
state.SessionID = sess.GetSessionID()
state.UpdatedAt = time.Now()
return store.Set(ctx, state)
}

Call saveState whenever you advance the form or update conversation context.

When a client reconnects with the same session ID, load state and resume:

func restoreOrCreate(ctx context.Context, store Store, sessionID string) (*SessionState, error) {
st, err := store.Get(ctx, sessionID)
if err != nil || st == nil {
return &SessionState{
SessionID: sessionID,
FormAnswers: make(map[string]string),
}, nil
}
return st, nil
}

Use the restored state to determine the next question, prompt, or agent context.

  • On session start: Call restoreOrCreate with your session ID from transport or backend. Pass restored state to your form orchestrator or agent.
  • On each turn: Update in-memory state, then call saveState.
  • On session stop: Perform a final saveState. Optionally delete state after a TTL.
OptionDescriptionDefault
Store backendRedis, PostgreSQL, DynamoDB, etc.Application-defined
TTLHow long to keep state after sessionApplication-defined
Key prefixNamespace for keys (e.g., voice:session:)Optional

The client may be sending a new session ID, or the store lost the data. Treat missing state as a new session and start from scratch. Log and emit metrics for monitoring.

Use the UpdatedAt field with a configurable maximum age. Clear or warn-and-restart when state exceeds the age threshold.

Transport or load balancer may assign different IDs on reconnect. Use a stable identifier (phone number, user ID + call ID) as the persistence key and map it to GetSessionID().

  • Error handling: Retry Get/Set with backoff to avoid failing session start on transient store errors
  • Monitoring: Use OpenTelemetry metrics for restore hits/misses, save latency, and store errors
  • Security: Encrypt PII in stored state and enforce access control by tenant or user