/** * Conversation Context Store * * Per-phone-number telephony session state: language, message history, * active flow, retry counts. Held behind an injected, synchronous * `ContextStore` (the structural-interface shape of `VoiceLimitsStore` - see * `src/voice/limits.ts` - though not its async contract; see the interface * doc below) so a host can swap the built-in `Map` for another in-process * implementation: an LRU with its own eviction policy, a store instrumented * for metrics/observability, or a test double. `createInMemoryContextStore()` * is this module's own default; `TalkerConfig.contextStore` only overrides * it when a host sets one (see `configureContextStore` and `src/mount.ts`). */ import type { FlowState } from "../flows/types"; import type { Channel } from "../types"; /** * Conversation context stored per phone number. */ export interface TelephonyContext { phoneNumber: string; channel: Channel; detectedLanguage: string | null; messageHistory: Array<{ role: "user" | "assistant"; content: string; timestamp: number; }>; activeFlow: FlowState | null; noSpeechRetries: number; lastPrompt: string | null; createdAt: number; lastActivity: number; } /** * Structural dependency for per-phone-number context storage - any object * with these methods satisfies it, the same shape as `VoiceLimitsStore`. * * Synchronous, unlike `VoiceLimitsStore`: every mutation in this module * (bumping `lastActivity`, pushing to `messageHistory`, and so on) writes * the updated `TelephonyContext` back through `set()` before returning, so * an implementation is free to clone/serialize on both `get()` and `set()` - * nothing outside this module holds onto a `TelephonyContext` across two * calls and mutates it directly. That rules out only a store whose * read/write genuinely cannot complete synchronously (a networked cache, a * remote database) - those would need an async interface, which is a larger * redesign this one does not attempt. */ export interface ContextStore { get(phoneNumber: string): TelephonyContext | undefined; set(phoneNumber: string, context: TelephonyContext): void; delete(phoneNumber: string): void; clear(): void; /** A snapshot or live view of all entries, for the cleanup sweep. Order is not significant. */ entries(): Iterable; } /** The default `ContextStore`: a plain in-memory `Map`, single-process only. */ export declare function createInMemoryContextStore(): ContextStore; /** * Swap the active `ContextStore`. Called by `src/mount.ts` (shared by * `createTelephonyRoutes`/`createStandaloneServer`) only when * `config.contextStore` is explicitly set - leaving it unset keeps this * module's own default, shared across mounts in the same process. Any call * replaces the store outright: entries already in the previous store become * unreachable, so call it before serving traffic, not mid-flight. */ export declare function configureContextStore(newStore: ContextStore): void; /** * Start periodic cleanup of stale contexts. `onTick`, when given, runs on * every tick alongside context expiry - a way for other in-memory stores * (e.g. call/pending's PendingQuery map) to reuse this single timer instead * of running their own. * * The timer is a module-level singleton: a second call while one is already * running is a no-op and its `ttlMs`/`intervalMs` are silently ignored (only * `onTick` would matter here anyway, since both mounts share the same * `ContextStore`). Mounting `createTelephonyRoutes`/`createStandaloneServer` * more than once in a process - two chatter instances, a test that doesn't * call `stopCleanup()` between setups - inherits the first mount's config; * this logs so that isn't silent. Call `stopCleanup()` first if a later * mount's config should actually take effect. * * Unref'd so a lone pending interval never keeps a standalone/CLI process * alive after everything else has finished - callers that do need to wait on * it (tests measuring ticks) already await other signals, not process exit. */ export declare function startCleanup(ttlMs: number, intervalMs: number, onTick?: () => void): void; /** * Stop periodic cleanup (for testing / shutdown) */ export declare function stopCleanup(): void; /** * Get or create a context for a phone number */ export declare function getOrCreateContext(phoneNumber: string, channel?: Channel): TelephonyContext; /** * Get context without creating one */ export declare function getContext(phoneNumber: string): TelephonyContext | undefined; /** * Set detected language (first detection wins). * * The language is LLM-derived from caller speech and sticks for the life of * the context, so a malformed code is rejected here rather than stored: it * would otherwise be reused as a path segment and an object key on every * turn until the context expires. Rejecting without storing keeps the slot * open for the next, well-formed detection. */ export declare function setDetectedLanguage(phoneNumber: string, language: string): void; /** * Get detected language for a phone number */ export declare function getDetectedLanguage(phoneNumber: string): string | null; /** * The language to render this caller's next phrase in. * * Detection runs on the caller's first utterance and sticks for the life of * the context, so every phrase lookup after that turn has a language to use. * This is the one accessor phrase call sites reach for: writing * `getDetectedLanguage(x) || "en"` at each site works until one site forgets, * and a single forgotten site is a caller who said one thing in French and * hears the next error, timeout or acknowledgment in English. * * Falls back to `DEFAULT_LANGUAGE` before detection has run (or for a number * with no context at all), which is what the phrase loader would resolve to * anyway - so the fallback is stated here rather than left implicit. */ export declare function resolveLanguage(phoneNumber: string): string; /** * Add a message to conversation history */ export declare function addMessage(phoneNumber: string, role: "user" | "assistant", content: string, channel?: Channel): void; /** * Get message history for a phone number */ export declare function getMessageHistory(phoneNumber: string): Array<{ role: "user" | "assistant"; content: string; timestamp: number; }>; /** * Clear all context for a phone number */ export declare function clearContext(phoneNumber: string): void; export declare function setActiveFlow(phoneNumber: string, flowName: string, params?: Record): void; export declare function getActiveFlow(phoneNumber: string): FlowState | null; export declare function updateFlowParams(phoneNumber: string, params: Record): void; export declare function clearActiveFlow(phoneNumber: string): void; /** * Internal call-flow bookkeeping for the no-speech ladder (see * `src/routes/call/handle-nospeech.ts`). Module-level, not package-level: the * package root no longer re-exports it, matching its read-only sibling * `getNoSpeechRetries`, which was never exported from the root at all. */ export declare function incrementNoSpeechRetries(phoneNumber: string): number; export declare function getNoSpeechRetries(phoneNumber: string): number; /** * Internal call-flow bookkeeping for the no-speech ladder (see * `src/routes/call/handle-respond.ts`). Module-level, not package-level: the * package root does not re-export it. */ export declare function resetNoSpeechRetries(phoneNumber: string): void; export declare function setLastPrompt(phoneNumber: string, prompt: string): void; export declare function getLastPrompt(phoneNumber: string): string | null; /** * Clear all contexts. A test-only reset helper, exported from this module for * this package's own tests and not from the package root. */ export declare function clearAllContexts(): void;