Algolia-Central
Documentation/09-Session-State.md
09 — Session State
Last updated: 2026-04-17. Schema verified from lib/search/redis.ts:20.
The SessionState interface
File: lib/search/redis.ts:20
Storage: Upstash Redis REST (one key per sessionId, prefixed rc2:)
export interface SessionState {
// 4 Onion Signals (core qualification)
stack?: string | null; // "React", "Shopify", "Vue"
scale?: string | null; // "startup", "enterprise", "1M+ records"
role?: string | null; // "developer", "merchandiser", "executive"
pain?: string | null; // "latency", "relevance", "complexity"
// 4 Business Entities (enrichment)
brand?: string | null; // Company name
industry?: string | null; // "e-commerce", "SaaS", "media"
product?: string | null; // "Search", "Recommend", "NeuralSearch"
architecture_concepts?: string[] | null; // array of tech concepts
// Discovery tracking
asked_signals?: string[]; // signals already asked (no-repeat)
turn_count?: number; // conversation turn counter
active_persona?: 'maverick' | 'elena' | 'bruno';
// F-045 — Conversation timeline written every Maverick/specialist turn
conversation_turns?: ConversationTurn[];
// Handoff state (persists between handshake + execute calls)
pendingHandoff?: {
expandedQuestion: string;
target: 'elena' | 'bruno';
} | null;
// Metadata
locked_count?: number; // computed
last_updated?: string; // ISO timestamp
}
interface ConversationTurn {
turn: number; // turn_count at time of emission
role: 'user' | 'assistant';
content: string;
persona?: 'maverick' | 'elena' | 'bruno'; // assistant turns only
timestamp: string; // ISO
extracted_attributes?: Record<string, any>;
}
Lifecycle
First turn
history.length === 0atorchestrator.ts:75- Triggers
resetSessionState(sessionId)— clears any prior state - Then reloads via
getSessionState()which returns defaults
Per turn (Maverick)
- Load:
getSessionState(sessionId)— ~41–53ms (warm) - Merge:
updateSessionState(sessionId, newSignals)after signal extraction — uses||operator (F-026 issue — never clears wrong values) - Track:
addAskedSignal(sessionId, signal)when discovery emits — appends toasked_signals[] - Count:
countLockedSignals(state)— counts non-null signal fields (treats "wrong value" as locked — also F-026 issue) - Increment:
updateSessionState(sessionId, {turn_count: +1})— final step - Append timeline (F-045):
appendConversationTurns(sessionId, [userTurn, assistantTurn])— writes both messages toconversation_turns - Persist handoff: if handoff detected,
updateSessionState(sessionId, {pendingHandoff, active_persona})— carriesexpandedQuestionto the subsequent consent-execute call
Per turn (Specialist)
- Load: same — also reads
pendingHandoff.expandedQuestionfor execute triggers - No writes to signals — specialist doesn't extract new signals, just consumes
- Writes telemetry only via
persistSessionTelemetry() - Append timeline (F-045):
appendConversationTurns(sessionId, [userTurn, specialistTurn])— specialist response joins the timeline withpersona: 'elena' | 'bruno'
Redis access layer
File: lib/search/redis.ts
| Function | Purpose | Timing |
|---|---|---|
getSessionState(id) |
Load with defaults | ~41–53ms |
getDefaultSessionState() |
All fields null + empty arrays | synchronous |
updateSessionState(id, partial) |
Merge into existing | ~40ms |
countLockedSignals(state) |
Count non-null signal fields | synchronous, O(1) |
addAskedSignal(id, signal) |
Append to asked_signals[] | ~40ms |
resetSessionState(id) |
Clear | ~40ms |
deleteSessionState(id) |
Remove key | ~40ms |
incrementCounter(name, ttl) |
General-purpose counter | ~40ms |
setCache(name, value, ttl) / getCache(name) |
L1-style cache | ~40ms |
appendTurnsPure(existing, new) F-045 |
Pure append helper (no Redis) | synchronous |
appendConversationTurns(id, turns) F-045 |
Read-merge-write turns to timeline | ~80ms (HGETALL + HSET) |
Signal lifecycle example (run 001–006 observed)
| Turn | User input | Locked signals (cumulative) | Dossier length |
|---|---|---|---|
| 0 | "Our customers search 'casual summer dress under 80'..." | 3 | 207 |
| 1 | "I'm a merchandiser, but I also understand deep technology..." | 5 | 259 |
| 2 | "We have roughly about half a million products." | 6 | 263 |
| 3 | "We have a custom in-house platform. We are planning to go to Shopify..." | 8 | 271 |
| 4 | "Yeah, several of them: - Mobile experience..." | 8 (QUALIFIED) | 271 |
Note dossier growth: +52 chars → +4 chars → +8 chars. Signals ARE being persisted. F-034: but Maverick prompts don't reference them strongly enough in the response.
Handoff state carry-forward
Critical mechanism for the two-call specialist flow:
- Maverick's qualification turn detects handoff → calls
updateSessionState(sessionId, {pendingHandoff: {expandedQuestion, target}, active_persona: target}). - Client receives
event: handoff_proposalwithrequiresConsent: true, shows "Bring in expert" button. - User clicks → client re-POSTs
/api/searchwithconsent: true, persona: target. - Backend enters consent branch (
api-src/search.ts:193) →igniteSpecialist({trigger:'execute'})→buildAgentStudioMessagereadssessionState.pendingHandoff.expandedQuestionand uses THAT as the effective query.
Without Redis persistence of pendingHandoff, the execute call would have nothing meaningful to ask the specialist.
Planned additions (Step 3+)
For F-033 (customer-story dedup):
referencedCustomers?: string[]; // [lowercased customer slugs already mentioned in this session]
For F-036 (anti-topic-repeat):
topicsCoveredInPreviousTurns?: string[]; // ['neural search', 'natural language understanding', ...]
For F-026 (pushback detector):
revisionCandidates?: string[]; // [signal fields that user contradicted — prioritize for revision in next discovery question]
These extend SessionState, read+write through the same updateSessionState path.
Known issues
| F# | Issue |
|---|---|
| F-026 | Merge logic (\|\| operator) + countLockedSignals treat wrong values as valid |
| F-033 | No referencedCustomers[] field → same customer namedropped 3× per session |
| F-036 | No topicsCoveredInPreviousTurns[] → "neural search, natural language" repeat |
Cross-references
- 03-Files-Reference —
redis.tsfile entry - 04-Functions-Reference —
getSessionState,updateSessionState,countLockedSignals - 02-Data-Flow — where session is loaded and written in the pipeline
- 10-Known-Issues — F-026, F-033, F-036