Remember the caller between calls - #4
Merged
Merged
Conversation
The agent forgot everything on hanging up: history was the last six turns of the current session, so a companion met the same person new each time. Long-term semantic memory only. A bounded set of natural-language facts about the caller — a category and one sentence — injected whole into the prompt and never searched. Retrieval was the obvious answer and does not fit: it runs inside the turn against a two-second budget, and it fires on similarity when what a companion must know is unconditional. Reasoning in ADR-0021. Reading costs one indexed local query on the turn path and no network call. Writing is a model call every N turns on a spawned task the turn never waits for (ADR-0022); a failure there leaves the stored set alone and the call unaffected. Facts are keyed on (user_id, character_id), so what one persona was told another does not know (ADR-0023). A caller can see what is held about them and delete it, at GET and DELETE /api/memory. A uid identifies but does not authenticate, so notes kept about a person have to be visible to them. Off unless configured. The [memory] section switches it on and bounds it; an absent section leaves it off, so a configuration written before this does not silently start sending notes to the model provider. CI now runs the agent and api suites, which is what gates any of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgGCfYp1iBEpD8iL4Dg5XQ
An ADR records the decision that was taken. Memory has more rejected options than taken ones — whether to retrieve or inject whole, rewrite or patch, close the category list or leave it open — and none of that was written down anywhere a person could find it. docs/memory.md carries both: what the four kinds of memory are and which exist here, how semantic memory is stored, read and written, and then thirteen decision points with the options and what each costs. The assumptions nobody has measured are listed as such, including the one that would overturn a structural decision. Linked from docs/README.md, architecture.md and the retrieval table in CLAUDE.md, so it is reachable rather than merely present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015SH5nghQRqj6797aj4FTVk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The agent forgot everything on hanging up. Conversation history is the last six
turns of the current session, so a companion met the same person new every
time. This adds semantic long-term memory only: what is true about the
caller, surviving the call.
What it does
A bounded set of facts about the caller — a category and one natural-language
sentence — injected whole into the prompt as one system message, and never
searched.
Facts are written by an extraction that runs every N turns on a spawned task the
turn never waits for. It reads the recent turns and the current set, asks the
model for a replacement set, and reconciles the difference in one transaction.
The three decisions
extracted fact set, injected whole; not retrieval. Retrieval runs inside the
turn against a two-second budget, and it fires on similarity when what a
companion must know — their name, their cat, what was promised — is
unconditional. It also retrieves transcripts, recognition errors included,
rather than conclusions.
extraction every N turns on a spawned task. Not at the end of the call,
because callers hang up and that end is not reliably observed.
on
(user_id, character_id). A persona referring to something it was nevertold reads as broken, which is worse than forgetting.
Cost on the turn path
One indexed local
SELECT, no network call. A read that fails is logged and theturn proceeds without it: memory failing makes the agent forgetful, never makes a
call fail.
Privacy
A
uididentifies but does not authenticate, and this now keeps notes about aperson.
GET /api/memoryshows a caller everything held about them acrosspersonas;
DELETE /api/memoryforgets it, optionally narrowed to one personawith
?character_id=. Both behind the existing token.docs/product.md§4 saysplainly that the notes exist, that a model writes them, and that they reach the
model provider on every call.
Off unless configured:
sonari.toml.exampleswitches it on so a clean clonedemonstrates the feature, while an absent
[memory]section leaves it off, so aconfiguration file written before this does not silently start extracting.
Tests
26 cases, listed in the task document and implemented one for one: injection and
its placement, that a broken store cannot fail a turn, persona isolation,
scheduling on the Nth turn and not the others, parsing and both caps and
deduplication, and that every way extraction can fail leaves the stored set
alone. The acceptance case drives a real turn, lets the scheduler spawn, and
asserts the fact reaches a second session's prompt. The reconcile SQL is tested
against a live database and skips itself without
DATABASE_DSN.CI previously ran only
cargo test -p harness, so none of this would have gatedanything; it now runs
cargo test -p agent -p apias well.Known costs, all recorded
The raw turns stay in
llm_messages, the set is small enough to read, andGET /api/memorymakes the loss visible.Not in this change
Short-term memory (the six-turn window, rolling summaries), episodic memory
("last time we talked about X"), and vector retrieval.
pgvectorstays unused;the image already allows it when episodic memory arrives.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NgGCfYp1iBEpD8iL4Dg5XQ