Agent memory
What an agent remembers between conversations, where each memory came from, and how memories are kept current as their sources change.
- See what a memory rests on
- Consolidate duplicate memories and undo it
- Catch a memory that makes an agent answer worse
- Choose what happens when a memory's source changes
- Re-check memories on demand
Agents keep two kinds of memory. Short-term memory is working context for a ticket or a follow-up question and expires on its own (72 hours by default). Long-term memory is what an agent has learnt: facts about a person, its own procedures, notes for the whole workspace. Long-term memory is distilled from finished conversations or written through the memory API, held for review until approved, and recalled into an agent's prompt when it is relevant.
Provenance: what a memory rests on
Every long-term memory records the sources it was learnt from, with a fingerprint of each as it was when the memory was written:
| Source | Recorded when | Changed when |
|---|---|---|
ticket | The memory is about a ticket | Its title, description, status, resolution or category changes, or it is deleted |
kb | The writer names a knowledge entry | The entry's text or resolution changes, or it is removed |
record | The writer names a structured record (source/type/id) | Its fields change, or its source deletes it |
memory | A memory derived from another | That memory is superseded, rejected or invalidated |
conversation | The distiller learnt it from a conversation | Never: a finished conversation does not change, and one that ages out of the transcript window is no reason to forget what was learnt |
Name sources when you write a memory by hand:
POST /api/memory
{"agent": "copilot", "scope": "agent", "text": "VPN-4012 is fixed by client 5.2.1",
"sources": [{"kind": "kb", "ref": "KB-7"}]}GET /api/memory-provenance/{id} shows what a memory rests on and each source's state at its last check.
When a source changes
Every 15 minutes (and on demand with POST /api/memory-provenance/check) each tracked source is checked again. A memory whose source changed or disappeared is stale, and what happens next is the TICKETIQ_MEMORY_INVALIDATION setting:
| Mode | What happens to a stale memory |
|---|---|
flag (default) | Audited as memory.stale and listed at GET /api/memory-provenance/stale. It is still recalled, so nothing an agent answers changes until you decide |
requeue | It goes back to the review queue with the reason ("re-check: its source ticket:INC-12 changed") and is not recalled until someone approves it again |
invalidate | It is retired as invalidated and never recalled again; the row is kept for lineage |
off | Nothing is checked |
Memories written before provenance existed have no recorded sources and are never touched. A contradiction is handled when it is found: approving a memory that contradicts or supersedes another retires the older one, and any memory derived from the retired one becomes stale here.
Every finding and every action is audited (memory.stale, memory.requeued, memory.invalidated, memory.provenance.check), by memory id and reason, never the memory's text.
Consolidation
The distiller writes one memory per thing it learns from one conversation, so the same fact told ten times becomes ten rows. Every 6 hours (and on demand with POST /api/memory-consolidations/run) each scope's active long-term memories of one category are tidied:
| Step | What happens |
|---|---|
| Duplicates | Memories that say the same thing once case and spacing are set aside: the newest is kept |
| Near-duplicates | Memories whose key terms mostly overlap are merged by the memory distiller into one statement that keeps every distinct fact. If the model is unavailable or over budget, only duplicates are folded |
| Summaries | A scope still holding more than 40 memories has its oldest summarised into one |
Each run writes a consolidation plan naming every memory before and after. Nothing is deleted: memories folded away are kept as superseded, and a merged memory records the ids it replaced. What a run does with its plan is the TICKETIQ_MEMORY_CONSOLIDATION setting:
| Mode | What happens |
|---|---|
propose (default) | Merged memories wait in the review queue and every original stays in use, so no agent's answers change until you apply the plan |
apply | Runs apply their own plans; any of them can still be reverted |
off | Nothing is consolidated |
GET /api/memory-consolidations lists the plans. POST /api/memory-consolidations/{id}/apply puts a plan in force, and POST /api/memory-consolidations/{id}/revert puts the before back exactly: the originals in use again and the merged memories retired. Runs, applies and reverts are audited (memory.consolidation.proposed, .run, .applied, .apply, .reverted, .revert) by ids and counts, never by what a memory says.
Eval gate
A memory recalled into an agent's prompt changes what the agent says, so a wrong one costs answers until someone notices. The eval gate catches it. It asks the agent's gate cases twice, once as the agent answers now and once with the new memory in its prompt the way recall adds it, and scores both answers against each case's expectations (expect.contains, expect.regex, expect.excludes). A case that passed without the memory and fails with it is a regression. Procedures are the rules an agent gives itself, so they are gated the same way.
Gate cases are an agent_turn eval dataset named memory-gate-<agent>, or memory-gate for every agent. Cases without expectations are skipped. An agent with no gate dataset is untested: nothing is called and nothing is spent. Each case costs two governed calls of the agent, so keep the dataset to the answers that matter (at most 20 cases are used).
What a verdict does is the TICKETIQ_MEMORY_ACTIVATION_GATE setting:
| Mode | What happens |
|---|---|
shadow (default) | Activation is exactly as before: a reviewer's approval, the distiller's auto-activation and a trusted write go into force at once. The check is queued and runs in the background every 15 minutes. A regression is audited as memory.gate.regressed and listed at GET /api/memory-gate/checks?verdict=regressed for you to act on |
enforce | The check runs before the memory goes into force. A memory that regresses a case, or whose check could not run, stays in the review queue with the failing cases as its reason. An untested memory goes into force, and its verdict is audited |
off | Nothing is checked |
POST /api/memory-gate/{id}/check runs the gate on one memory now. It records the verdict and changes nothing else. Every verdict is audited (memory.gate.pass, .regressed, .untested, .error) by memory id and case ids, never by what the memory or the answers say.