Vision1 of 5
What she can see
On this page
Kotoba has vision. An image reaches her in one of two ways: you hand her one, or a tool she runs
gives one back. Both end up in the same place — an input_image part on the request that goes to
your model provider — and both are subject to caps that are enforced in code, not suggested.
Nothing here has an OCR step or a local vision model. She sees an image because the provider's model sees it. That is worth knowing before you send it anything.
Handing her an image
| Where | How | What it accepts |
|---|---|---|
| Browser | The paperclip in the chat composer | Any image/*, plus PDF and plain-text files |
| Terminal | /attach ~/Downloads/shot.png, or ctrl-v | .png .jpg .jpeg .webp .gif .pdf ride the turn; any other text file is copied into her working folder for read_file instead. ctrl-v takes PNG, JPEG, GIF or RIFF bytes off the system clipboard |
| Discord | Attach it to your message | Anything whose content type starts with image/, plus PDF |
The browser sends the file to POST /api/session/{session_id}/attachment as a base64 data URL. The
terminal and the Discord bridge call the same in-process store directly. In all three the part waits
in memory until the next turn goes out, and that turn carries it.
The terminal is the odd one out for a reason a Wayland session forces: a paste hands a terminal
application text and only text, so the CLI shells out to wl-paste, xclip or pngpaste and reads
the clipboard itself. With none of the three installed, ctrl-v says so instead of doing nothing.
The caps, and what happens at each one
| Limit | Value | What you get when you hit it |
|---|---|---|
| Files in one message | 4 | The browser gets HTTP 409 and the sentence "I can only carry 4 files in one message — send this one and I'll take the next lot after it." The terminal prints the same sentence and draws no sent row. Nothing is stored. |
| Size, browser | 14,000,000 bytes of data URL | HTTP 413, attachment too large. Base64 costs a third, so the real ceiling on a picture is about 10.5 MB. |
| Size, terminal | 14,000,000 bytes, checked twice | Once against the file, once against the data URL — so again about 10.5 MB. She says the file is too big and tells you to leave it in her working folder, where read_file can reach it. |
| Size, Discord | 8,000,000 bytes of the attachment | The name goes on a refused list she is told about, so she says out loud she could not take it. |
| PDFs, browser | 5 pages | The page count is guessed in the browser before the upload; a longer PDF is refused there. |
The per-message cap bounds one message, not the session. Send the first four, then send the rest.
After the turn
Two stores hold what you shared, and they answer different questions.
- The pending queue is drained by the turn that uses it. One shot, gone.
- The shared list keeps the last 3 images of the session, up to 40,000,000 bytes across all sessions, so the model has a handle it can quote back later. It is shown base64 it can never repeat, so without this list "save that picture I sent" had nothing to point at. The newest image is never the one evicted.
In local voice mode the pending queue is drained only by a typed turn. A spoken turn, and the silence turns that fire when you say nothing, leave it alone — otherwise a sentinel turn would eat the upload before the message it belonged to went out. The browser sends an attachment as a typed message, so this is invisible in normal use.
Both stores live in the backend's memory. Restart the backend and both are gone. In the browser and on
Discord, an image you attach is never written to disk unless she saves it herself with
remember_image. The terminal is different: /attach and ctrl-v also copy the
picture into her file library under shared/, which is what lets her open it again days later.
Images she gets herself
Nothing captures on a timer or in the background. An image arrives only as the result of a tool call inside a turn, and today that means MCP tools: the browser server's screenshot, or any MCP tool whose result carries an image block. No built-in tool takes a picture of anything.
Two caps apply at that door:
- at most 4 images out of one tool call;
- an image over 10,000,000 base64 characters (about 7.5 MB) is dropped, and the text of the result is kept.
A dropped or capped image is silent to you: the tool result is still delivered, just without it.
Whatever survives is filed into her Files library and logged as a session capture.
Two tools return images without being captures: view_capture and recall_image re-open a picture
that already exists. The loop excludes both by name from the filing step, or every recall would
duplicate the image in Files.
What the model actually receives
A tool result carrying images is not sent as a string. It becomes a content-part list: one
input_text part with the result text, then one input_image part per image at detail: "auto". An
image with no text of its own still leads with a placeholder line, so no part is empty.
That request goes to whichever provider you configured — OpenAI or xAI today, or any endpoint that speaks the same Responses API. The image leaves your machine. See Privacy and deletion.
Elision: images do not stay in view
Within a single turn the loop rewrites its own history to keep the token cost down.
Every browser view — a snapshot, a screenshot, a run_code or evaluate page inspection — is
replaced by this stub as soon as a newer one arrives:
[Earlier browser view elided to save context — take a fresh browser_snapshot for current refs.]
Only the latest browser view survives, and the stub replaces the whole result, image parts included.
Ordinary browser calls like navigate and click are small and are left alone.
Across turns, nothing survives at all. Conversation history is reloaded from the turns table, whose
content column is text; the words she wrote about a picture come back, the picture does not. The
pending attachment queue was drained by the turn that used it.
So the state a follow-up question meets is this:
| What was in front of her | Still there next turn? | What brings it back |
|---|---|---|
| An image you attached | No | remember_image(source="attachment") still reaches the bytes through the shared list. At the terminal, view_capture reaches it too |
| A screenshot from a tool | No | view_capture(file) — it is on disk in Files |
| A keepsake she saved | No | recall_image(query) |
This is the entire reason the two stores in the next two pages exist. Without them, "what was in that post you just read?" has one honest answer: send it again.
Next: Session captures.
