Commit Graph

3 Commits

Author SHA1 Message Date
e90bc33d0f Memory hygiene: a relevance floor, no duplicates, and expiry that actually deletes
Some checks failed
CI / test (push) Failing after 4m40s
CI / fixture (push) Failing after 8s
Three things that decide whether memory improves with use or rots with it.

A RELEVANCE FLOOR. Recall returned its top five whatever they scored, so a run
about shift cover was handed five memories about certifications simply because
nothing better existed — and the block tells the model these are things the
workspace remembered, so it reads them as pertinent. Embeddings are
unit-normalised, so knowledge_dot is cosine, and 0.30 is where text is usually
about something else. A judgement rather than a measurement, and the honest way
to tune it is to watch what gets carried on real questions.

NO DUPLICATES. The same standing preference comes up in conversation after
conversation, and each run that hears it has no idea the last one wrote it
down. Five recall slots spent on one fact restated five ways is the normal
failure, not a rare one. A write with the same normalised text, in the same org
and about the same subject, pushes the existing memory's expiry out instead of
adding a row — matched on the same sentence rather than a similar one, because
collapsing two genuinely different facts is the worse error.

EXPIRY THAT DELETES. expires_at was set and filtered on read, and nothing ever
removed anything: the row was invisible and still retained. "We keep it ninety
days" has to be true of the table, not only of the query. Prune is batched, and
a redaction is kept for a thirty-day grace period so an erasure stays provable
shortly afterwards.

It runs in the maintenance sweeper that already exists rather than a second
scheduler — same ticker, same cancellation, same failure isolation. That forced
one honest change: Maintenance() used to be nil without OAuth, on the reasoning
that there was nothing to sweep. There is now, and a retention promise enforced
only when an unrelated feature happens to be enabled is not a promise. The test
that asserted the old behaviour now asserts the new one and says why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:29:28 +05:30
43dabb5f72 Switch memory on: the agent may keep one, and carries them into the next run
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 8s
The store and the write trigger existed and nothing used either. This wires
both ends, so "we have long-term memory" stops being a statement about code
that exists and becomes one about behaviour.

READ. Memories are recalled before retrieval and placed before it in the
prompt: it is the smaller block and the more general one — a standing
preference frames how the documents should be read, where a document does not
frame a preference. The question stays last, because a model reads the last
thing and answers it, and evidence after the question becomes the prompt.

Skipped for smalltalk on the same terms as retrieval. Nobody needs remembering
to say good morning, and paying for it is how "hi" came to cost six thousand
tokens.

FAILS QUIET, RECORDED LOUDLY. A memory store that is unreachable must not take
the run with it: an answer without memory is worse, not wrong, and the
alternative is an outage in the knowledge layer becoming an outage in the
product. The trajectory records the failure, and records separately when the
store returned recency instead of relevance — a reader comparing two answers
needs to know which one got which.

WRITE. tools.Remember is registered only where there is somewhere to put it,
through DefaultToolsWithMemory rather than a nil check inside the old
constructor: a deployment that has not migrated 000017 must not offer a tool
whose every call fails against a table that is not there. Passing nil
registers exactly the catalogue that was there before.

Memory shares retrieval's embedder, and memory.Embedder is knowledge.Embedder
by structure so it cannot be given a different one. Two embedding models in
one deployment produce vectors that cannot be compared, and the failure is
silent: a recall that returns nothing rather than an error.

Five new tests on the loop, including the two that matter — a failing store
still answers, and a greeting carries nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:09:59 +05:30
8c51c22c86 Add long-term memory: org-scoped, attributed to a subject, and expiring
The first store whose contents are deliberately fed back into a prompt, which
makes it a different kind of table from everything around it. Org-scoped by
product decision: a memory written while one recruiter worked is available to
the next, because a workspace's view of its own hiring should not reset per
seat.

It remembers both kinds asked for — operational facts and observations about
named people — and the second is why most of this code is provenance rather
than payload. "This applicant seemed unreliable", stored automatically and
read into a later hiring answer, is profiling under GDPR and is the artefact an
employment claim is built on. The only thing that makes holding it defensible
is that it can be listed, shown and erased, so:

  - subject_type and subject_id are mandatory for anything personal, refused at
    the door rather than defaulted, because a memory about somebody that names
    nobody cannot be shown to them or deleted for them;
  - Held() answers a subject access request and Forget() answers an erasure,
    each in one statement, and Forget is a soft delete so the erasure itself is
    recorded;
  - every memory carries its author and the run that wrote it, so "why did it
    say that" survives memory entering the picture, and an inference is never
    read back as if a person had written it;
  - everything expires. Ninety days by default: a hiring workspace changes
    shape over a quarter, and a stale fact read as a current one is worse than
    no memory at all.

The block the model sees is fenced and labelled on the same terms as retrieved
documents, for a stronger reason — a memory is text this system wrote about its
own users, so a model that treated it as an instruction would let one run steer
every run after it. It states the origin of each line and says plainly that a
memory is never a reason on its own to accept or reject anybody. That sentence
is pinned by a test.

Recall is semantic where an embedder exists and newest-first where it does not,
and says which happened rather than quietly returning recency. Five memories by
default: this competes for the same prompt as the tool catalogue and the
retrieved block, against a ceiling of 8,000 tokens a minute.

Migration 000017 is WRITTEN AND NOT APPLIED. Nothing is wired into the runtime
yet — this is the store and its rules, reviewable on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 19:55:08 +05:30