Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

Her character7 of 8

What she remembers about you

About 7 minutes to read

On this page

Two stores, with different rules, both readable and editable by you.

WhereWhat is in itFormat
Profileuser_profile table in the SQLite databaseYour name. That is all — measured, only the key name is ever writtenone row per key
Memory facts~/.kotoba/memory/Everything else she has keptplain markdown

Neither is the conversation log. That lives in the database as turns, with its own full-text search (session_search), and is not what this page is about.


The profile is one field

Three places call upsert_user_profile, and all three pass the key "name":

  • kotoba setup, when it asks what to call you
  • POST /api/settings/user-name, from the browser's first-run screen
  • the memory extractor, when you tell her your name in conversation

It reaches the prompt as a bulleted list under # What you know about the user:

# What you know about the user
- name: Alex

With nothing stored, that block reads (nothing known about the user yet).

The user_profile table is a general key/value table and could hold more. Today it holds one key.

Clearing it: there is no route and no command. An empty value is a deliberate no-op —

"An empty value is a no-op rather than an erasure — first run offers a skip, and a skip must not delete a name she already knows."

To remove it you edit the user_profile table in the database directly. Overwriting is easy: re-run kotoba setup, or just tell her — but note that the browser's Settings panel has no field for your name. Settings → Personality sets hers. The only browser route to yours is the first-run screen.


The memory store

Jailed under KOTOBA_MEMORY_DIR, default ~/.kotoba/memory, deliberately outside the repository.

~/.kotoba/memory/
  USER.md              the index
  topics/pets.md       one file per topic
  topics/work.md
  topics/preferences.md

USER.md opens with a header that is addressed to you, not to her:

"Durable memory, organized by topic — about you and about anything important (projects, plans, context). Kotoba reads this index every conversation; details live in the linked topic files (she can recall any of them on demand) and she creates new topics freely as things come up. You can edit any of these."

A real one, generated by writing three facts:

markdown
## Recent

- Has a cat named Luna
- Works as a security engineer
- Favorite color is cobalt blue

## Topics

- pets → topics/pets.md (1 facts)
- preferences → topics/preferences.md (1 facts)
- work → topics/work.md (1 facts)

A topic file is a heading and a list of - bullets. Nothing else in the file is read.


What goes into the prompt

Not everything. facts_for_prompt() returns the ## Recent bullets, each tagged with the topic file it lives in, plus one pointer line. Measured output:

Has a cat named Luna [pets]
Works as a security engineer [work]
Favorite color is cobalt blue [preferences]
(This list is the 3 most recent of 3 facts, across 3 topics: pets (1), preferences (1), work (1).
 Use memory_recall for anything not shown — it is saved, just not inline.)

## Recent is capped at 25 entries (RECENT_CAP). Everything older is still in its topic file and still reachable through memory_recall, it is just not in front of her by default.

The pointer line is bounded on purpose. Unbounded it "grew past a thousand characters across 54 free-form topics, and with no total it read as 'this is everything', which is the opposite of true".

Nothing from either store reaches the prompt when the speaker is not you. load_context takes a personal flag; the Discord bridge passes personal=bool(who and who.is_owner), and when it is false both the profile and the facts are empty strings. The comment states why withholding the tools would not have been enough: "they are already IN the prompt, and the only thing between them and the room is a sentence asking her not to tell."


How a fact gets written

Two paths.

She calls memory_write. She decided the thing was worth keeping.

The extractor. After every reply, a fire-and-forget background pass reads your message and asks a small model for durable facts, your name, and any explicit renaming of her. It is told what is already stored so it can de-duplicate across languages, and it is told what not to keep:

"NEVER extract any of these — they are the conversation, not the person… something they asked you to DO… a passing reaction, a joke, a provocation, an insult… an inference about their identity, health, beliefs or politics that they did not plainly state about themselves."

The extractor's output also runs through _looks_ephemeral, a filter for task narration ("creating the folder…", "running the script…"). That filter is off for an explicit memory_write, because there she has already decided.

Two refusals that are code, not instruction

memory_write returns a refusal rather than saving when:

  • The fact is compound. A semicolon, or a coordinator opening a new clause. "a compound fact can never be corrected later without destroying its other half."
  • The fact looks Spanish. The store is English-only because the duplicate detector works within one language. The check is deliberately hard to trigger — a Spanish-only word, or a Spanish verb plus one more signal — and it covers no other language: "Spanish is the only non-English this store has ever received; anything else passes as before."

Facts are capped and normalised

One fact is one line. Whitespace is collapsed; anything past 400 characters is truncated at a word boundary with an ellipsis. Multi-line text is refused at the source, because it "opens a stray '#' section in the system prompt and truncates the Recent scan, permanently dropping every later index entry".


Corrections and duplicates

This is the most careful code in the store, and its bias is stated up front:

"Every judgement here is biased toward KEEPING, since retiring is a permanent delete with no archive: a wrongly kept fact is visible clutter, a wrongly dropped one is invisible and self-sealing."

A new fact can do four things to what is already there:

Result
It is a near-duplicateNot written. The tool tells her what is stored instead
It refines an existing fact (the old one's keywords are a proper subset)Written; the vaguer one is retired
It corrects one (same frame, one value swapped, same class)Written; the stale one is retired
It contradicts one that cannot be safely retiredWritten; the conflict is reported, both are kept

Measured, on a store that already held "Favorite color is cobalt blue":

python
>>> write_fact("Favorite color is moss green", "preferences")
{'written': True, 'reason': 'written',
 'retired': ['Favorite color is cobalt blue'], 'conflicts': [], 'duplicate_of': None}

Each guard exists for a real defect: two friends the same age where one lost the other, a sister's address retiring your own as a "refinement", "the dog is 4 years old" retiring the cat's age. The comparison is not about how much two facts overlap but where they differ — a swapped verb is a rewording, a swapped subject is another statement.

Search is stemmed, so "where do I live" finds "Lives in Madrid". Stemming is applied on the search side only and never to the duplicate machinery, because "a wider notion of sameness there deletes facts nobody contradicted".


Reading and editing it yourself

It is markdown in your home directory. Open it.

Adding a bullet to a topic file works — measured:

topic file now:      ['Works as a security engineer', 'Plays bass in a band']
in the prompt?       no
findable by search?  yes  →  memory_recall / search_facts return it
recall('work'):      both facts

So a hand-added fact is real memory. It is just not inline in the prompt until it appears in ## Recent.

Adding a line under ## Recent in USER.md puts it inline immediately. It shows untagged, because it has no topic file to name — which is correct, not a bug: "A Recent entry retired since the last index rebuild has no topic and is left bare rather than mislabelled."

Deleting a bullet removes the fact. The index is rebuilt on the next write, so a stale ## Recent entry may linger until then; delete_topic and the write path both filter Recent against what still exists.

Writes are atomic and locked across processes — the lock file USER.md.lock sits beside the index. Editing while she is running is safe in the sense that you will not corrupt the file; it is not safe in the sense that a write of hers can land between your read and your save.


Clearing memory

One topic, from the browser: Settings → Memory lists every topic as slug · count with a delete button. It calls DELETE /api/memory/topic/{slug}, which removes the file and rebuilds the index.

One topic, by hand: delete topics/<slug>.md.

Everything: delete ~/.kotoba/memory/. It is regenerated empty on the next write.

One fact: edit the topic file, and the bullet under ## Recent if it is there. There is no route for a single fact.

There is no undo. _retire_facts drops bullets from every topic file that matches, and nothing is archived.


What each surface shows you

SurfaceWhat you see
Browser, Settings → MemoryEvery topic, its fact count, a delete button per topic
Terminal, /settings memoryEvery topic as slug — N things I've kept. Read-only
The filesEverything, in full

The Settings API payload also carries the eight most recent facts (memory.recent), but the panel does not currently draw them — topics and counts are all it renders.


Two things this store is not

  • It is not visual memory. Images she has kept live separately (visual_memory), with transient session captures beside them.
  • It is not the conversation log. "The conversation log and full-text search stay in SQLite/FTS5; this module owns the facts." Deleting a topic does not delete the conversation it came from, and deleting the database does not delete these markdown files.