Documentation menu
Memtru Afterlife
A delete can succeed, be confirmed, and still be undone later. An in-flight write lands after the delete, or an ingestion cycle rebuilds the memory from conversation history that was kept. Afterlife tests that: it deletes a synthetic marker, proves it is gone, then drives the lifecycle events that come later and checks every retrieval surface after each one.
memtru test --scenario afterlife # both scenarios, against your configured adapter
memtru demo --afterlife # broken store, then fixed store, then the comparison
Interactive walkthrough: /demo/afterlife.
Scenarios
MT-AF-001 Late write resurrection
Requires MEMORY_WRITE, MEMORY_SEARCH, LIFECYCLE_PENDING_WRITES, and MEMORY_DELETE or THREAD_DELETE.
- Write a memory containing a unique synthetic marker; confirm the owner can retrieve it (positive control).
- Start an update of that memory and hold it in flight (
begin_pending_write). - Delete the memory. Confirm the marker is absent on every surface (
immediate_check). - Release the held write (
release_pending_writes). Poll every surface. - If the adapter supports
restart, restart the isolated instance and poll again.
FAIL late_write_resurrected when the marker is retrievable after step 4 or 5. The finding names the event that preceded the reappearance.
MT-AF-002 Session re-ingestion resurrection
Requires MEMORY_SEARCH, MEMORY_DELETE, LIFECYCLE_CONVERSATION, and LIFECYCLE_NEW_SESSION or LIFECYCLE_CONSOLIDATE.
- Record a conversation in which the user asks to remember a unique synthetic marker; run ingestion; confirm the derived memory is retrievable (positive control).
- Delete the derived memory. The conversation itself is retained. Confirm the marker is absent.
- Start a new session; poll every surface.
- Record an unrelated message and run ingestion; confirm the unrelated message became a memory (proof that ingestion ran); poll every surface.
- If supported, restart and poll again.
FAIL history_reingestion_resurrected when the marker reappears. The marker is never sent again after deletion, so a reappearance can only come from retained state.
When the result is not PASS or FAIL
| Situation | Result |
|---|---|
| The adapter lacks a required hook or deletion contract | SKIPPED |
| The marker was never retrievable before deletion | INCONCLUSIVE (positive control) |
| The marker was still visible right after deletion | INCONCLUSIVE; run MT-DEL-003, this is not resurrection |
A hook reports executed=False, or did not process the held write |
INCONCLUSIVE |
| Ingestion ran but did not turn the unrelated message into a memory | INCONCLUSIVE |
| A hook raised a provider error | ERROR |
PASS is only possible when every lifecycle event in the run reported that it ran.
Deletion contracts
Memory deletion and thread deletion are different promises.
memory_record:delete(ctx, id)removes a memory. Afterlife expects it to stay removed.thread_checkpoint:delete_thread(ctx)removes conversation / checkpoint state (for example LangGraph'scheckpointer.delete_thread). It is not expected to remove long-term memories derived from that thread.
MT-AF-001 uses memory_record when MEMORY_DELETE is available and thread_checkpoint otherwise, and the report says which. MT-AF-002 always requires memory_record. See tests/test_langgraph_afterlife.py::test_thread_deletion_is_not_memory_deletion.
Lifecycle hooks (memtru.adapters.lifecycle)
Adapters opt in by also inheriting MemoryLifecycleHooks and declaring the matching capabilities. Every hook is optional.
| Hook | Capability | Must do |
|---|---|---|
begin_pending_write(ctx, content, target_id=) |
LIFECYCLE_PENDING_WRITES | Start a real write and hold it before it is applied. |
release_pending_writes(ctx) |
LIFECYCLE_PENDING_WRITES | Let held writes complete exactly as the system would. |
record_conversation(ctx, messages) |
LIFECYCLE_CONVERSATION | Append user messages to retained history. |
start_session(ctx) |
LIFECYCLE_NEW_SESSION | Begin a session, including any session-start ingestion. |
consolidate(ctx) |
LIFECYCLE_CONSOLIDATE | Run one ingestion / consolidation cycle. |
restart() |
LIFECYCLE_RESTART | Restart the isolated test instance. |
retrieval_surfaces(), inspect_surface(ctx, surface, query) |
LIFECYCLE_SURFACES | Expose further surfaces an agent reads (for example a profile summary). |
delete_thread(ctx) |
THREAD_DELETE | Delete thread / checkpoint state. |
Rules:
- Return a
HookReceiptfrom every hook.executed=Falsemakes the scenario INCONCLUSIVE. - Put every record a hook creates in
receipt.created; Memtru tracks it and deletes it during cleanup. - Act only inside
ctx.namespace, and restart only an isolated instance. - Declare a capability only if the hook drives the real mechanism. The engine drops lifecycle capabilities from adapters that do not implement
MemoryLifecycleHooks. Thehttpadapter implements them through optional lifecycle endpoints, so a service in any language can run Afterlife by implementing those endpoints and advertising the capabilities.
Report format
Afterlife results carry an optional afterlife object (schema 1.0, additive). Results of other scenarios omit the key, so earlier readers are unaffected.
"afterlife": {
"deletion_contract": "memory_record",
"hooks_executed": ["release_pending_writes"],
"surfaces_checked": ["profile"],
"events": [
{"seq": 4, "phase": "delete", "action": "delete", "detail": "memory mem_000003 deleted", "elapsed_ms": 1, "evidence_step": 6},
{"seq": 5, "phase": "immediate_check", "action": "immediate_check", "detail": "marker absent from every checked surface", "marker_visible": false, "surfaces": [], "elapsed_ms": 2},
{"seq": 7, "phase": "observe", "action": "after_release", "detail": "marker visible on search, profile", "marker_visible": true, "surfaces": ["search", "profile"], "elapsed_ms": 4}
],
"resurrected": true,
"first_resurrection_event": 7,
"resurrection_trigger": "release_pending_writes",
"reproduce": ["memtru test --scenario MT-AF-001", "memtru demo --afterlife"]
}
Phases: create, positive_control, pending_write, delete, immediate_check, lifecycle, observe. The dashboard shows the timeline inside the run's result row and on the finding page; the HTML report shows it under the finding and in an "Afterlife timelines" section.
Verified integrations
| Target | MT-AF-001 | MT-AF-002 | Evidence |
|---|---|---|---|
| Built-in store, correct | PASS | PASS | tests/test_afterlife.py |
Built-in store, late_write_resurrection |
FAIL (release) | PASS | tests/test_afterlife.py |
Built-in store, history_reingestion |
PASS | FAIL (session start) | tests/test_afterlife.py |
| LangGraph 1.2.16 app, no tombstones | FAIL (release) | FAIL (session start) | tests/test_langgraph_afterlife.py |
| Same app, tombstones honoured | PASS | PASS | tests/test_langgraph_afterlife.py |
Mem0. The Mem0 adapter supports MT-AF-001 through its pending-write hook; MT-AF-002 is skipped because Mem0 ingestion requires an LLM. Results against Mem0 are withheld from this page while they are handled with the maintainers.
LangGraph 1.2.16. LangGraph does not extract memories itself. examples/afterlife-langgraph is an application built on real LangGraph primitives (StateGraph, InMemorySaver, InMemoryStore, checkpointer.delete_thread) with a deterministic memory manager. The failing variant rebuilds the profile from every retained thread and applies deferred writes blindly; the fixed variant writes tombstones. The defect is an application pattern, not a claim about LangGraph.
pip install -e "packages/memtru[langgraph]"
cd examples/afterlife-langgraph && memtru test # FAIL, exit 1
# set adapter.options.honour_tombstones: true # PASS, exit 0
Limitations
- Observes retrieval surfaces the adapter exposes. It does not prove erasure from backups, replicas, logs, embeddings, caches or other copies Memtru cannot see.
- Exercises only the lifecycle events the adapter exposes as hooks. A nightly job that is not exposed is not tested.
- A PASS holds for the configured consistency window after each event, not for every timing.
- Pending writes are controlled fixtures. Real races can involve timings a hook does not reproduce.
- Ingestion is driven through hooks; the extraction quality of a real model is out of scope.
- Over HTTP, lifecycle events are only as faithful as the service's endpoints: an endpoint that reports
executed: truewithout driving the real mechanism cannot be detected.