Memory and long conversations
A model's context window cannot hold an entire conversation or every note about a person. Polychrome provides conversation-history tools and adds persona notes according to their scope and the current audience.
The conversation's own history is its signed log. The agent searches that history with a tool when earlier messages leave the context window. A person's notes are facts reconstructed from their memory journal. Turn assembly selects these notes and adds them to a system message without search. The scope predicate determines which notes the audience may receive.
Scope determines whether memory is eligible, regardless of its relevance. Direct and unspecified scopes require proof that you alone read the output. Portable memory is eligible in any conversation.
Conversation history and persona notes
| What it holds | Where it lives | How it reaches a turn | Who can see it |
|---|---|---|---|
| This conversation's own history | the conversation's own append-only signed log | the agent asks for it mid-turn, with a read-only tool | this conversation and nothing else |
| Notes about a person | that person's own append-only memory journal, one per person, held by the state plane | prepended once at turn assembly as one system block, never searched | only where the scope rule permits |
History tools let the agent retrieve earlier messages beyond its current context window. Persona-note selection reads the memory journal separately from conversation-history search.
Scope determines who may receive a note
Scope determines eligibility before ranking. Direct notes are available only when the subject is the sole human audience member. Unspecified notes follow the same restriction. Portable notes are available to every audience because the person chose to make them non-private. The conversation where a note was learned does not determine its read scope.
The read audience determines whether private notes are eligible. A shared conversation does not become private because only one person speaks during a turn. The scope predicate checks for ReadAudience::SubjectAlone.
CI restricts access to unscoped notes
Reading notes without scope filtering requires an explicit infrastructure accessor. Five files may call it: the fold, extraction, merge, memory-control RPCs, and forensics. A build check rejects callers outside the allowlist. Prompt-facing reads use scope-filtered selection. See the fold.
How a note gets written
Notes come from post-turn extraction or an approved tool call.
After the turn. A separate extraction pass runs after the turn commits, without delaying its reply. It replays only committed events and skips turns already processed, including turns that produced no notes. The pass asks a model for new notes and contradictions against at most 24 ranked active notes. Before writing, it rechecks whether the conversation was erased or changed to incognito. Extraction runs serially for each person to avoid concurrent writes based on stale notes. See the extraction pass.
During the turn. The agent can request a note write through a tool that requires human approval. The turn's attribution supplies the person and source conversation; tool arguments cannot select another person. Neither extraction nor explicit writes record notes from an incognito conversation.
Contradictions preserve note history
A contradiction closes the old note's validity interval and preserves both notes for replay. A restatement adds a source to the existing note. The fold starts with the extractor's initial confidence and adds 500 basis points per independent corroborator, capped at 10,000. Replay therefore reproduces the same confidence value. Merging preserves scope boundaries so private and portable notes do not combine. See the merge pass.
Periodic maintenance
A periodic pass rebuilds the profile document from active notes and removes exact duplicates, retaining the oldest. It retires transient notes after four hours. Notes marked as lasting do not expire based on age.
What enters model context
Turn assembly adds one system message without persisting it in the transcript. The block has a budget of about 1,200 characters. It prioritizes language, tone, sorted preferences, notes, and the recent-conversation digest, in that order. When space runs out, it drops digest content first, then notes, before profile content. The selection includes at most 12 notes and 5 digest lines.
The block reserves space for the notes' explanatory caveat. If the heading and caveat cannot fit, it omits the notes. An observational heading separates notes from standing preferences. When the audience cannot receive private memory, a separate notice always states that restriction, regardless of whether private notes exist. See how the block is assembled.
Looking back in this conversation
The agent can search earlier messages in the current conversation and read matching turns in full.
# conversation_find: find matching earlier messages
{ "query": "the deploy plan we agreed on", "limit": 5 }
# Returns matching messages from this conversation, newest first
{ "matches": [ { "turn_id": "abc…", "excerpt": "…let's ship it behind a flag…" } ],
"truncated": false }
# conversation_find, pattern mode: match literal text or a regular expression
{ "query": "INC-\\d+", "mode": "pattern" }
# Returns matching messages, newest first, with excerpts around each match
# No matches returns an empty result
{ "matches": [], "truncated": false,
"hint": "nothing in this conversation matched; try fewer or different words, or switch mode to see what is there another way" }
# conversation_read_turn: read a matching turn
{ "turn_id": "abc…" }
# Returns turn text; long turns have their middle section truncated
{ "turn_id": "abc…", "text": "…", "truncated": false }| Tool | What it answers | Bounds |
|---|---|---|
conversation_find | returns matching messages from this conversation, each with a turn id and excerpt | relevance or pattern mode; 5 matches by default, 25 at most; truncated and a hint when more matched |
conversation_read_turn | reads one earlier turn in full, by its id | long turns have their middle section truncated and report truncation |
conversation_read_tool_result | returns a recorded tool result by call id | excised results return a removal notice without the original content |
conversation_recent_turns | returns recent turns and their ids | — |
conversation_list_tool_calls | returns recorded tool calls and their ids | — |
Search ranks messages by the number of matching words, with newer messages first when scores tie. It uses lexical matching, without a vector store. No matches returns an empty result with a suggestion. An unknown turn id returns an error. See the navigator.
Reads stay within the current conversation
The control plane binds history reads to the current conversation. The tools accept no conversation argument and ignore scope fields supplied by the model. Recall similarly uses the turn's attribution to select the person. Search excludes content an admin has excised. Reading an excised result returns a removal notice. See how removal works.
Recall uses the same selection rules
The recall tool uses the same fold, scope rule, and ranking as turn assembly. It returns 12 notes by default, at most 25, plus 5 digest lines. Recall takes no search query. See the recall module.
Empty notes and failed reads
If turn assembly cannot read notes, the turn proceeds without them. Recall distinguishes an empty store from a failed read:
| State | Recall response |
|---|---|
| No notes selected in a conversation where private memory is readable | "nothing has been kept about this person yet — memory_write is how a note gets here" |
| The audience cannot read private notes | Recall reports the audience restriction without revealing whether private notes exist. |
| The notes could not be read this time | "your notes aren't readable right now, so nothing was recalled; carry on from what this conversation already tells you — the rest of the turn works normally" |
A failed read does not establish that no notes exist. The recall module defines both response strings.
Inspect, edit, and erase notes
Listing and deleting notes use the person resolved from the request and accept no alternative target. Deleting a note closes its validity interval while retaining the audit record. Erasing a person deletes their entire memory store.
Deleting a conversation removes notes whose text originated there. Other conversations that corroborated a note cannot preserve its text after deletion of the source conversation. Incognito conversations produce no notes. See Personas.