Article
Zota: From YouChannel to Memory-Led Practice Surfaces
A code-led walkthrough of Zota as the successor to YouChannel: session memory design, context compaction, and model-generated frontend surfaces rendered by stable Next.js components.
Zota is the project I built after YouChannel.
YouChannel treated an authentic YouTube video as the object of learning. The product had a concrete source, a transcript, derived study material, and a loop that turned media into practice. That was useful, but the media object also became the ceiling. The learner still had to translate a video lesson into the room they actually cared about.
Zota keeps the useful part of that architecture and changes the object. Instead of a YouTube video, the product starts from a real upcoming scene: an interview, client meeting, presentation, thesis defense, sales pitch, panel, or difficult conversation. The job is not to summarize content. The job is to remember the learner’s situation and keep generating practice surfaces around it.
From YouChannel to Zota
The product change is small in wording and large in architecture.
| Design question | YouChannel answer | Zota answer |
|---|---|---|
| What is the learning object? | A real video and its transcript. | A real future communication scene. |
| What should the model preserve? | Useful content, vocabulary, and lesson structure. | Scenario facts, learner patterns, debrief evidence, live drill summaries, and deadlines. |
| What does the frontend render? | Media-derived learning views. | Model-generated training surfaces tied to session state. |
| What makes a return visit useful? | The video context and derived material are still there. | The advisor remembers the room, prior weakness, and next drill. |
This is why Zota is not mainly a “language tutor chat.” The chat is only one surface. The durable product is the combination of scenario state, session memory, tool outputs, feedback rows, artifacts, and Live summaries.
Start from the real event
Zota opens in one advisor thread and asks for the actual moment the learner needs to survive: interview, client meeting, defense, pitch, panel, or Q&A.
- Durable state
- session row, advisor thread, empty session memory
- Code evidence
- createSessionWithAdvisorSeed, AdvisorPage
Crystallize the situation
When enough detail exists, the advisor stores a structured scenario with participants, register, goals, target phrases, difficulty, phases, and event date.
- Durable state
- sessions.scenario_json, sessions.event_date
- Code evidence
- crystallize_scenario, update_scenario
Practice one hard moment
Before full roleplay, the advisor isolates a pressure point and turns it into one small speaking move that the learner can attempt immediately.
- Durable state
- recent thread turns, scenario phase
- Code evidence
- warmup_coach capability prompt
Enter the counterpart role
The assistant calls start_simulation, becomes the interviewer or audience member, and stays in character until the round naturally ends or the learner stops.
- Durable state
- tool output inside advisor thread
- Code evidence
- start_simulation, detectSimulationState
Turn pressure into feedback
After a simulation, Zota records rubric scores, strengths, weaknesses, and quote-grounded highlights instead of leaving feedback as loose chat prose.
- Durable state
- session_feedback rows
- Code evidence
- record_debrief, TrainingLogPage
Create targeted practice assets
Phrase banks, drills, and cue cards are generated as byproducts of observed weaknesses, not as generic lesson content.
- Durable state
- session_artifacts rows
- Code evidence
- create_artifact
Escalate to spoken pressure
When the learner is ready, start_drill hands a structured brief to Gemini Live so the browser can run an immersive voice round.
- Durable state
- ephemeral Live token, transcript segments
- Code evidence
- start_drill, /api/advisor/live-session
Code evidence: the app starts from a brief zota/app/(app)/app/page.tsx, components/session/scenario-composer.tsx, app/(app)/actions.ts
/app renders ScenarioComposer and QuickStartGrid. Submitting a text or voice seed calls createSessionAction, creates a session, initializes # Session Memory, optionally inserts the opening user message, and redirects to /s/[sid]/advisor.
Memory Is the Product Boundary
The biggest Zota design choice is that the model is not treated as the database.
There are two different memory surfaces:
Visible thread
The learner sees a conversation
agent_threads.messages stores UI messages, compact cards, Live summary cards, tool outputs, and the latest run pointer. It is the interface history.
Durable memory
The model reads selected state
session_memories.raw_markdown stores the session memory that should survive compaction, voice drills, and return visits. It is the coaching state.
That split matters because a long UI thread is not the same thing as useful context. A training product needs to preserve facts like “the interview is next Thursday,” “the learner softens ownership language under follow-up pressure,” and “the Live drill exposed weak metric answers.” It does not need to resend every old sentence forever.
| Write path | What enters memory | Why it exists |
|---|---|---|
| Session seed | # Session Memory at session creation. | Gives every workspace a memory document from the first turn. |
save_memory | Typed entries: user, feedback, project. | Lets the advisor preserve recurring patterns and deadlines. |
| Context compact | Extracted memory entries from older UI turns. | Keeps model input bounded without dropping coaching history. |
| Live end | A ## Live Session ... markdown block. | Converts spoken practice into future-readable evidence. |
Code evidence: memory entries are explicit product state zota/lib/zota/advisor-tools.ts, lib/zota/advisor-memory-entries.ts, lib/zota/memory-repo.ts
The save_memory tool upserts typed entries into session_memories.raw_markdown. Entries are named, updated by name, and separated from ordinary UI turns.
Context Compaction
Zota uses a bounded model window and a durable memory document at the same time.
The current policy:
- keep the last 32 UI messages for normal advisor input,
- after a compact run, keep a smaller 16-message tail,
- trigger compact when estimated full UI message JSON reaches 100,000 input tokens,
- write older findings into session memory instead of replaying the whole thread.
Full-thread estimate
72k Below compact threshold. The model still receives only the latest 32 messages.agent_threads.messages can still keep compact cards and folded rows for the UI. The advisor model does not need to see all of them. It should see the recent tail plus the session memory that was intentionally extracted.
Code evidence: compact gate runs before chat send zota/components/session/advisor/advisor-chat-transport.ts
DefaultChatTransport.prepareSendMessagesRequest estimates context size, calls /api/advisor/compact when the threshold is reached, folds older local messages, inserts a compact card, and sends the narrowed message list to /api/advisor/chat.
Code evidence: compact writes structured memory zota/app/api/advisor/compact/route.ts, lib/zota/advisor-memory-compact.ts
The compact route selects dropped messages, asks a model to extract memory entries, upserts those entries into session_memories.raw_markdown, and returns a compact result for the timeline card.
This is the YouChannel lesson in a different body. The model should read selected state, change selected state, and write back the parts that matter. It should not be asked to remember the whole product by having every old token pasted into every future request.
Model-Generated Frontend Surfaces
The second important design is how Zota lets the model create product UI without letting the model own the UI.
The model does not generate arbitrary React components. Instead, it generates structured objects with known schemas. The frontend owns rendering, spacing, permissions, empty states, and navigation. This keeps the product inspectable and avoids turning every model response into an unsafe mini application.
Model output becomes entry cards
- Model contract
{ templates: [{ id, emoji, title, description, seedText }, ...six total] }- Frontend surface
- QuickStartGrid renders locale-aware scenario cards on /app. Clicking one starts an advisor session with the generated seedText.
- State boundary
- Cached by unstable_cache. It is not a user record until the learner starts a session.
- Code evidence
- lib/zota/scenario-templates.ts, components/session/quick-start-grid.tsx
A fuzzy brief becomes a session model
- Model contract
{ title, setting, participants, register, communicationGoals, targetPhrases, difficulty, phases, eventDate }- Frontend surface
- AdvisorScenarioPanel and the page header render the scenario as the current workspace, not as loose chat text.
- State boundary
- Stored in sessions.scenario_json with versioned updates through advisor tools.
- Code evidence
- lib/zota/advisor-tools.ts, components/session/advisor/advisor-scenario-panel.tsx
Tool results render as timeline components
- Model contract
{ toolName: "save_memory" | "create_artifact" | "start_drill", result: structured JSON }- Frontend surface
- AdvisorToolCard switches on the tool name and renders memory, artifact, search, progress, drill, and simulation cards.
- State boundary
- Saved inside the advisor UI thread. Durable writes also land in memory, artifacts, feedback, or scenario rows.
- Code evidence
- components/session/advisor/advisor-tool-cards.tsx
Generated artifacts become an inspectable page
- Model contract
{ kind: "phrase_bank" | "drill" | "cue_cards", title, phaseId, body } plus rubric feedback- Frontend surface
- TrainingLogPage renders stats, score lines, debrief rows, and artifact tabs from stored rows.
- State boundary
- Stored in session_artifacts and session_feedback, scoped to the session and user.
- Code evidence
- components/session/advisor/training-log-page.tsx, lib/zota/artifact-repo.ts
A drill brief controls a voice surface
- Model contract
{ characterToPlay, questionsToAsk, pressurePoints, targetDurationMinutes, briefForDrillPartner }- Frontend surface
- The Live sheet uses the brief to start a spoken drill. The ended call returns as a summary card and memory block.
- State boundary
- Live token is ephemeral. Transcript summary is appended to session_memories.raw_markdown.
- Code evidence
- lib/zota/advisor-tools.ts, app/api/advisor/live-session/route.ts, app/api/advisor/live-end/route.ts
The pattern appears in several places:
| Surface | Model-generated contract | Frontend-owned rendering |
|---|---|---|
| Quick-start cards | ScenarioTemplate[] with id, emoji, title, description, seedText. | QuickStartGrid renders six locale-aware starter cards on /app. |
| Scenario panel | ScenarioModel written by crystallize_scenario and patched by update_scenario. | AdvisorScenarioPanel renders the current scene model. |
| Timeline tool cards | Tool outputs such as save_memory, create_artifact, search_notes, start_drill. | AdvisorToolCard switches by tool name and renders known components. |
| Training log page | session_feedback rows and session_artifacts rows. | TrainingLogPage renders stats, charts, debriefs, and artifact tabs. |
| Live drill sheet | start_drill brief with character, questions, pressure points, and partner instructions. | The Live UI starts a constrained voice session and returns a summary card. |
Code evidence: quick-start cards are generated as data zota/lib/zota/scenario-templates.ts, components/session/quick-start-grid.tsx
generateQuickStartTemplates calls generateObject with a Zod schema and returns exactly six scenario template cards. The app renders those cards; the model never writes the page component.
Code evidence: artifacts become a page, not loose chat zota/lib/zota/advisor-tools.ts, app/api/advisor/artifacts/route.ts, components/session/advisor/training-log-page.tsx
create_artifact stores phrase banks, drills, and cue cards in session_artifacts. The training log route reads those rows and renders them as tabs alongside feedback history.
This is the core frontend idea: the model generates page material, not page code. The component boundary stays deterministic. The generated material can be cached, persisted, searched, audited, and rendered in multiple places.
Orchestrator, Tools, and Pages
Zota still needs an agent loop, but the loop exists to protect product state.
An advisor turn is split into:
- A shared context build: session, scenario, recent messages, memory, simulation state, timezone.
- An orchestrator plan: the model returns an
ExecutionPlanthat chooses capabilities and tone. - An executor prompt: selected capability files and context become the main advisor instructions.
- A tool loop: tool calls write scenarios, memory, feedback, artifacts, or drill briefs.
- A frontend render: stable React components turn those writes into cards, panels, and pages.
Code evidence: plan and execution are separated zota/lib/zota/orchestrator/types.ts, lib/zota/advisor-agent.ts, lib/zota/orchestrator/build-executor-prompt.ts
createAdvisorChatAgent builds shared context, calls the orchestrator, builds the executor prompt from selected capability files, creates a traced tool-loop agent, and records the orchestrator plan in agent_runs.input_data.
| Tool | Durable effect | Surface it unlocks |
|---|---|---|
crystallize_scenario | Writes sessions.scenario_json and session title. | Scenario panel and session workspace identity. |
record_debrief | Inserts session_feedback. | Training log stats, chart, and debrief rows. |
create_artifact | Inserts session_artifacts. | Artifact tabs and timeline artifact card. |
save_memory | Upserts structured memory entries. | Future advisor context and memory card. |
start_drill | Returns a voice-drill brief. | Live drill sheet and spoken practice surface. |
search_notes | Searches session memories. | Search result card inside the advisor thread. |
Live Voice as Memory Input
Live voice is where the memory design gets tested. A spoken drill is useful only if the next advisor turn knows what happened.
The handoff is intentionally narrow:
- The advisor creates a drill brief, or the server generates a thread brief from recent context.
- The server mints a short-lived Gemini Live token with constrained instructions.
- The browser runs the voice session.
- On end, transcript segments are summarized into a Live Session markdown block.
- That block is appended to
session_memories.raw_markdownand shown as a card in the advisor thread.
Advisor prepares the drill
- Durable state
- start_drill tool output with briefForDrillPartner
Server mints a Live token
- Durable state
- auth token, expireTime, newSessionExpireTime
The browser runs the spoken round
- Durable state
- input and output audio transcription segments
End call becomes memory
- Durable state
- /api/advisor/live-end, session_memories.raw_markdown
Advisor closes the loop
- Durable state
- record_debrief, session_feedback, optional artifacts
Code evidence: Live end becomes session memory zota/app/api/advisor/live-end/route.ts, lib/zota/advisor-live-summary.ts
The Live end route converts transcript segments into a structured summary, formats a markdown block with findings, hypothesis updates, next strategy, optional drill questions, pressure points, and transcript, then appends it to session memory.
The voice call is transient. The memory result is durable. That is the product boundary.
Deployment and Documentation
I also organized the Zota repo so the implementation can be handed off without relying on old notes.
| Document or surface | Purpose |
|---|---|
README.md | Short project entry, docs links, local run summary. |
docs/Zota-Docs-Index.md | Current docs index and maintenance rules. |
docs/Zota-Flow.md | Route, advisor, memory, tools, Live, generated surfaces, and data model. |
docs/Zota-Usage.md | User path, developer commands, env vars, current boundaries. |
docs/Zota-Deployment.md | Local, Supabase, Vercel, and Cloudflare Pages deployment. |
static-site/ | Public static explanation page for the project. |
The app runtime is a normal Next.js deployment with Supabase and model provider secrets. The static explanation page is separate, so the project can have a public readable page even when the real product is authenticated.
What Is Still Fragile
Planner quality
The orchestrator plan is still model output
The plan has a JSON contract and fallbacks, but capability selection still affects product behavior.
Generated surfaces
Schemas need to stay tight
The frontend is stable only if model output remains constrained and validated before rendering.
Voice evidence
Live summaries depend on transcript quality
A sparse transcript cannot support a rich memory update without inventing facts.
Memory hygiene
Saved observations need pruning rules
Typed memory entries are useful, but a real product still needs merge, delete, and user-visible review flows.
These are product risks worth naming because they point to the next work: stricter schemas, better plan validation, memory review UI, trace inspection, and cost controls.
The Useful Lesson
Zota is not interesting because it is another AI language-learning chat.
It is interesting because it treats memory and frontend generation as product primitives. The model remembers by writing selected state, not by owning every old turn. The model generates surfaces by emitting structured data, not by owning the React tree.
That is the line I wanted to draw after YouChannel: keep the real object, keep the practice loop, but make the learner’s upcoming room the center of the product.
The code is public at github.com/larry-xue/zota. The static explanation page is packaged in static-site/, and the canonical project reference on this site is Zota AI Communication Practice Console. The earlier YouChannel static archive is at youchannel-product.pages.dev.