Command Palette

Search for a command to run...

Hectal
PHASE 4Intermediate ~38 min· topic 8 of 8

Topic 4.8

Build: Mitra Remembers

In one line

Mitra becomes a real multi-turn assistant: a reducer holds each chat, every request sends a trimmed history, several chats are listed and can be switched while an answer streams, and the conversations are saved and survive a reload.

Think of it like this

A shop assistant who starts keeping a customer notebook. Before, every customer was a stranger. Now there is a page per customer, the assistant reads the recent notes before answering, a customer can come back tomorrow and find their page, and the assistant never writes a note on the wrong page. Mitra v4 gets that notebook.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Mitra v4
The shop assistant after this phase: multi-turn, many chats, saved.
chatsReducer
The outer reducer. It routes each action to the chat named by chatId.
sendable()
The function that decides which messages go in the next request.
Controller map
A ref holding one AbortController per chat that has a request running.
Interrupted
An answer that was streaming when the page closed. It is restored as cut off.
Scripted visitor
Lines at the end of an example that click and type for you.

Step by step

01The route and what it receives

The route is the same /api/chat, now with the whole trimmed history in messages. The stand-in model answers from what it receives, so it can recall a name only if the client sent it. In a real app this is the one place that calls the provider with your key (Topic 1.5).

server partwhole filejs
serve("/api/chat", fakeOpenAI(({ text, messages }) => {
  // ...answers from the messages it received, like a real model would
}, { delayMs: 15, firstDelayMs: 40 }));

02State: chats, each with its own stage

chatsReducer handles the list itself (new, select, delete) and passes every other action to the right chat's chatReducer. The title is set the first time a question is sent to an empty chat.

03Sending: capture, guard, trim, dispatch

send() does these in order. It returns if the text is empty or this chat already has a request (the ref check). It captures chatId and makes the ids. It stores a new AbortController. It builds the body from sendable(). Only then does it dispatch send and start the request.

Tokens, done and error all carry the chatId captured at the start, so switching chats cannot misdirect them. AbortError is ignored. finally removes the controller so the chat can send again.

04Saving and restoring

save() writes a versioned object with only id, title and messages. init() validates it, resets every chat to idle, and turns streaming and pending messages into error. The scripted visitor ends by throwing the screen away mid-answer (root.unmount()) and mounting a new one, which calls init() again. In this preview the storage is an in-memory copy that resets on each Run, so the "reload" happens inside one run; in a real browser it also survives a real reload.

05The tour

Press Run and read the console. The visitor says a name and asks for it back, starts a second chat, switches away while an answer streams, comes back, stops one answer, breaks one stream, crashes the server, and finally "reloads" mid-answer. Each console line is one of the scenarios listed above.

Then use it yourself: type your own messages, switch chats, delete one, press stop.

The tourdiagram
Rendering diagram…

Try it yourself

  1. 1

    Raise the budget

    In either example change BUDGET = 40 to 400.

    Nothing is trimmed any more and the notice never appears, so the model keeps seeing the name. The price is a bigger request on every turn.

  2. 2

    Ask your own things

    After the tour, type "My name is Ravi", then "What is my name?". Then open a new chat and ask "What is my name?".

    The new chat answers "I don't know your name yet.": chats do not share history, because each request carries only its own chat.

  3. 3

    Delete a chat that is streaming

    Ask "Where is ORD-1043?", and immediately click the x next to that chat.

    The chat disappears, its request is aborted, and nothing is written anywhere. This is the cleanup from Topic 4.5.

Code & diagrams

What Mitra v4 really sends to the model

A failed answer and a stopped answer are not sent, and the oldest messages are trimmed. Look at the last question: the name was said in the very first message, so after trimming Mitra can no longer answer it. That is exactly why the screen shows a notice.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

in the chat: 8 messages
good enough to send: 6
sent after trimming to the budget: 4
user: Try again
assistant: ORD-1043 (Ginger biscuits) is still being processed.
user: ORD-1042 please
user: What is my name?
does the model still see the name Asha? false
one huge message alone: 1 sent
Mitra v4

The visitor runs the whole tour by itself, then you can type. The last three lines are the "reload": the page was thrown away while an answer streamed, and the chats came back from storage. In this preview the storage resets on every Run, so only the reload inside one run is shown.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

empty: Start a new chat to ask Mitra. | No chats yet.
chat 1: My name is Asha / Nice to meet you! / What is my name? / Your name is Asha.
viewing chat 1 while chat 2 streams: Where is ORD-1… … | My name is Ash…*
back in chat 2: Where is ORD-1043? / ORD-1043 (Ginger biscuits) is still being processed.
stopped: ORD-1042 (Masala [stopped] | Status: done
broken stream: ORD-1043 is being pa [cut off] | The answer was cut off. Please try again.
server error: (no answer) | Mitra could not answer. Please try again.
long chat: Mitra no longer sees the first 1 message.
after reload: Where is ORD-1…* | My name is Ash…
question kept: true | interrupted answer shown as cut off: true
status after reload: Status: idle | send enabled: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Restore interrupted answers as they were

In the second example, change (m.status === "streaming" || m.status === "pending" ? { ...m, status: "error" } : m) to just m.

terminal
$ Console
── what you'll see ──
question kept: true | interrupted answer shown as cut off: false
status after reload: Status: idle | send enabled: true

Myth vs fact

Myth

Saving the chats means I saved Mitra's state.

Fact

Only the messages. Stage flags, errors and controllers describe the moment and are rebuilt at load.

Myth

A bigger app needs a bigger architecture than a reducer and a ref.

Fact

This whole app is one reducer pair, one request function and one storage pair. The SDK in Phase 5 removes some of it, but the ideas stay.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Resend does not need the whole history on the wire forever: Phase 5 shows servers that load history by chat id, so the client sends one message.

  • ▸

    Add a test for each console line in the tour. They are the scenarios your users will find first.

  • ▸

    Before adding features, check that send() still captures chatId at the top. Every later feature (retry, regenerate, tools) should reuse the same capture.

Remember this

  1. 1

    The problem this solves: After Phase 3, Mitra could stream, stop and recover, but each question stood alone and a reload erased everything. This topic joins Topics 4.1 to 4.7 into one app, so a customer can say their name, ask about an order, switch to another chat, close the tab and come back.

  2. 2

    Where we start. Mitra v3 (Phase 3) is one component with one answer: a route /api/chat, a reading loop, stop, retry and a rate-limit countdown. We keep the same route, element ids and error texts, and change what is behind the screen: the single answer variable becomes a list of messages in chats.

  3. 3

    What this version is made of. chatReducer (Topic 4.2) handles one chat. chatsReducer wraps it and routes each action by chatId (Topic 4.5). sendable() builds the request from the chat's done messages and trims to a budget (Topic 4.3). init() and save() handle storage with a version and mark interrupted answers (Topic 4.4). A ref holds one AbortController per chat for stop, delete and the double-send guard (Topic 4.7). readSSE() from Phase 2 reads the stream.

  4. 4

    Every scenario this version handles. *Memory:* the name given in turn 1 is answered in turn 2. *Trim:* with a tiny budget of 40 estimated tokens, old messages stop being sent and a notice says so. *Switch:* leaving a chat that is streaming, and coming back to a finished answer. *Stop:* the text so far stays, marked [stopped]. *Broken stream:* the partial text stays, marked [cut off], with a message. *Server error:* "(no answer)" and a message a person can act on. *Reload mid-answer:* the chat list returns, and the interrupted answer comes back as cut off, not as running. *Empty:* no chats gives a clear start screen.

  5. 5

    What Mitra can do now that it could not before: hold a conversation, keep several, and survive a reload. What is still missing, and where it is added: the hand-written plumbing is replaced by an SDK that manages messages for us (Phase 5); answers are still plain text, so Markdown, code and links need safe rendering (Phase 6); answers can't be cards (Phase 7) or take actions (Phase 8); there is no protection against hostile input (Phase 9), no polish for accessibility and mobile (Phase 10), no usage limits or metrics (Phase 13), and chats are saved only in this browser.

  6. 6

    Two shortcuts to know about. The history budget here is 40 estimated tokens so the trimming is visible in a short demo; a real one is thousands, set from your model's window minus the longest reply (Topic 4.3). And save() runs after every state change; a real app throttles it while streaming (Topic 4.4).

Explain it without notes

01

Walk through what happens from pressing Send to the answer being saved.

02

Why does Mitra v4 not forget when you switch chats?

03

What does Mitra v4 do after a reload in the middle of an answer?

Practice

01

Add a "Clear chat" button that is disabled while the chat is busy. Which reducer needs a new case?

02

Make the budget a prop and show "Mitra sees the last N messages" instead of the number hidden. What function already has the number?

Trade-offs

  • ↔

    Everything in the browser makes this version free to run and impossible to share across devices. Server-side chats (Topic 4.4) add sync and cost a backend.

  • ↔

    Hand-written plumbing teaches what an SDK does and costs about 200 lines. Phase 5 replaces much of it, and you will recognise each piece.

Done when you can

  • I can describe how the reducer, the history trimming, the controller map and storage fit together.

  • I can switch chats during a stream without misplacing tokens.

  • I can show every scenario in the tour: stop, cut off, server error, trimmed history, reload.

  • I can say what Mitra v4 still lacks and which phase adds each missing piece.