Treat Voice-Companion Memory as a Consent Ledger, Not Prompt History A developer demonstrates a TypeScript design pattern for voice-companion memory that treats user consent as a ledger rather than storing raw prompt history. The approach, applied to a Tencent RTC Conversational AI voice companion, uses bounded memory slots and explicit consent states to keep the application authoritative over durable facts. A personalized voice companion creates an uncomfortable trade-off: users do not want to repeat themselves, but they also do not want a misheard sentence to become a permanent “fact.” That tension is often hidden by calling conversation history memory . The implementation then retrieves old text, inserts it into a prompt, and trusts the LLM to interpret it correctly. A safer design gives memory to the application, not the model: This tutorial builds that boundary in TypeScript and shows how it fits a Tencent RTC Conversational AI voice companion. We will use a social companion that can remember a preferred name, music genre, and conversation style—but not arbitrary instructions. Keep the live-media pipeline and the memory lifecycle separate: Microphone │ ▼ Real-time voice session / speech recognition │ recognized turn ▼ Application turn coordinator ─────► LLM provider │ │ │ proposed typed memory │ response text ▼ ▼ Consent ledger Speech synthesis │ └──── confirmed facts only ────────► future LLM prompts Tencent RTC's Conversational AI documentation describes real-time voice interaction with multiple LLM providers. Its LLM configuration guidance also covers OpenAI-compatible models, agent platforms such as Dify and Coze, and request identifiers for routing and observability: The RTC layer can carry the live conversation, but your application should remain authoritative over what becomes durable memory. For this example, the model can suggest one of three bounded slots: | Slot | Accepted values | Suggested lifetime | |---|---|---| preferred name | A short name | Until revoked | music genre | An application-owned enum | 30 days | chat style | brief , balanced , or detailed | Until revoked | The model cannot store: This is intentionally less flexible than writing arbitrary text into a vector database. That loss of flexibility buys inspectability, predictable prompt construction, and a meaningful consent interaction. mkdir voice-memory-ledger cd voice-memory-ledger npm init -y npm install --save-dev typescript tsx @types/node mkdir src Add scripts to package.json : { "scripts": { "test": "tsx --test src/ .test.ts", "demo": "tsx src/demo.ts" } } Create tsconfig.json : { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true } } A useful memory record needs more than a key and value. It also needs provenance, consent state, scope, expiration, and replacement history. Create src/memory.ts : js import { createHash, randomUUID } from "node:crypto"; export const musicGenres = "classical", "electronic", "folk", "hip-hop", "jazz", "pop", "rock", as const; export const chatStyles = "brief", "balanced", "detailed" as const; type MusicGenre = typeof musicGenres number ; type ChatStyle = typeof chatStyles number ; export type MemoryValue = | { key: "preferred name"; value: string } | { key: "music genre"; value: MusicGenre } | { key: "chat style"; value: ChatStyle }; export type MemoryStatus = | "proposed" | "confirmed" | "rejected" | "superseded" | "revoked"; export interface MemoryRecord { id: string; subjectId: string; sessionId: string; sourceTurnId: string; sourceDigest: string; requestId: string; memory: MemoryValue; status: MemoryStatus; createdAt: number; confirmedAt?: number; expiresAt?: number; supersededBy?: string; } export interface ProposalInput { subjectId: string; sessionId: string; sourceTurnId: string; sourceTranscript: string; requestId: string; memory: MemoryValue; } export type ConfirmResult = | { ok: true; record: MemoryRecord } | { ok: false; reason: "not-found" | "wrong-session" | "not-proposed"; }; export class MemoryLedger { private records = new Map