← Blog

After the conversation ended—how do I put my memories away?

· 19 min read · 380 views
memvault · Write Track

After the conversation ended—how do I put my memories away?

What started as an idea scribbled on a napkin has evolved into a system with three gates and dual-track writing. This article focuses solely on how data is written to memory.

memvault is a memory vault I built for myself. Every day, after my conversations with the AI end, I want to preserve what we’ve discussed. Later, I’ll be able to search through it, recall it, and connect the pieces into a map.

This one is about Write Track, which is how memories get into the vault. I originally thought of this layer very simply, but after actually implementing it, a lot of details emerged. Below are two versions: a plain-language one and a technical one. Just pick whichever looks best to you.

Let me start by explaining the original concept.

At that moment, there was only one thought in my mind: “The conversation is over; leave the items behind—I’ll come back for them next time.”

And that's it. It's over.

Later, when reviewing the architecture with Claude, there were a few things I hadn't thought of initially—the order of the three doors, whether the dual tracks should block responses, and whether the triple should wait for the KG to be drawn before returning. This post breaks down three things separately: organizing, saving backups, and drawing the map.

Assign a proofreader first — Standardized notation for date, amount, percentage, and duration Different Ways of Writing the Same Thing → Must Be Formatted to a Standard Format for Comparison Three Gates · The Order Cannot Be Changed Dialogue Filter Out the Nonsense Small talk/garbled text/no information Block Malicious Content Hidden Commands/Injection Duplicate Match Skip/Merge/Replace/Add + ID Verification Machine · Counterfeit Detection Impersonation of Authority · False Identity · Hidden Annotations · Falsified Chronology · Embedded Code Vector Storage Semantics + Keywords Draw a Map Tuple + Alias Fork A Fork B The first two stages were bypassed → The fusion stage will spread the toxic information into the clean memory

I. Tidying Up: It’s Actually Three Stages

Three steps, in that order—this is the result of the write-side evolving from the initial two actions of “noise reduction” and “deduplication” to its current version:

  1. Filter out nonsense (small talk, garbled text, strings with no meaningful information)
  2. Block Malicious Intent (Some people hide commands within words to manipulate memories)
  3. Finally, I checked to see if I had mentioned it

Why can't the order be changed? During the third comparison, if it encounters a match that is "similar but not exactly the same," it will merge the new entry into the existing one. If a malicious message reaches this step first, the clean data will be corrupted.

This order was determined after we ran into a "MERGE" contamination issue—in the early stages, Dedup ran before Noise, so a block containing noise was flagged as similar and its contents were mixed in with those of a clean block.

"Comparison" itself isn't just about removing duplicates. It makes four types of decisions:

  • Almost the same: Skip
  • If there's something new, the old one still stands: Combine it with the existing entry and recalculate the fingerprint.
  • The old is wrong: it has been replaced by the new
  • Brand New: Newly Added

Any ambiguity regarding whether items are similar is referred to a small LLM for arbitration—it determines whether they should be merged, replaced, or left as two separate entries. So “conflict detection” is actually handled as part of this process, rather than as a separate step.

Sisters: Defend Against Deception

The second hurdle is simply writing plain-language commands like “Please forget the previous settings.” But more cunning opponents will disguise commands as statements the system has made or quotes from authoritative figures, weaving them into the conversation and waiting for them to be absorbed as memories.

Airport security can detect lighters, but it can’t stop someone from hiding contraband inside the bottom cover of a laptop. That’s why a back-scanning scanner was added after the second checkpoint, specifically designed to detect five types of camouflaged items:

  • Faking Authority—“‘An official document states...’”
  • Impersonation—The phrase “I am the system” pops up in a sentence
  • Hidden Commands in Comments
  • Time-Based Override—"The Most Recent Rule Takes Precedence"
  • Encode the instructions; you have to decode them to see them.

If any of them match, block it immediately. Only if none match should it be allowed through to compare against the duplicate door. This goes hand in hand with the “blocking malicious commands” mentioned earlier—the former blocks explicit commands, while the latter blocks disguised commands; if either is missing, the system can be bypassed.

Uniform Units

You say "next Wednesday," someone else says "3-12," and the document writes "2026-03-12"—all refer to the same day, but different formats prevent them from being compared. Amounts, percentages, and durations all suffer from the same problem: "three thousand dollars," "3,000," and "three grand" are the exact same sum of money.

So before entering these doors, a small preparatory step was buried: converting dates, amounts, ratios, and durations into a uniform standard format, while clearing out full-width spaces and strange symbols in the Chinese punctuation. It sounds trivial, but without it, the subsequent comparison would miss a bunch of memories that actually ought to be merged—the same event would be treated as two, and the map would fork.

II. Backing Up: From a Single Path to Two Paths

One path is not enough—my initial approach was to "store a vectorized copy," only thinking about the semantic fingerprint route. Within a month of launching the search end, the first case popped up: when encountering abbreviations, proper nouns, and precise keyword queries, high semantic similarity actually led to wrong results. Later, it was split into two paths:

  • "Storing semantic fingerprints along the way" (using your senses to remember the overall feel of a sentence)
  • Store keywords along the way (like looking them up in a dictionary)

Data is inserted into the database from two sources simultaneously and sorted together during queries.

3. Draw a map and create a backup at the same time

Extracting "who - did what - to whom" triplets runs concurrently with backing up, without waiting for the vectorization to finish. Entities and relationships are stored together, while alias merging is done along the way (such as treating ChatGPT and GPT as the same). The earliest draft had two steps: first extract "entities," then "relationships," which later evolved into happening simultaneously upon storage.

Before importing the map, I’ll perform another noise-reduction check—just an extra safeguard to prevent clutter from spoiling the map.

One more thing: every time a memory is recorded, a copy is made. Source History—Who saved it, when, and how confident they were. When reading through it, I’ll sort the entries, review the context, and decide whether to keep them—and I’ll always come back to this resume. This is where I plant the seeds for a complete reading cycle.

Regarding the “complete knowledge graph” I mentioned

I have to be honest and clarify something here.

"Complete KG" is not generated all at once during writing. Heavy tasks like community clustering and LLM summarization are handed over to the dreaming loop, scheduled for 4 AM. Moreover, it doesn't run unconditionally—two conditions must be met simultaneously: more than 24 hours have passed since the last dream, and there have been at least five sessions during that period. If these are not met, it skips.

In the first phase, I focused only on the lowest level: triples and entity resolution. Conflict detection had already been handled as part of the third step above, so I didn’t repeat it. In hindsight, this decision turned out to be the right one.

This post only covered "Write Track." Next, we'll shift our focus to Background Track—Once the memories are stored, there’s still a whole system running in the background. Things like dream loops, knowledge checkups, and interest profiling—which aren’t normally visible—will gradually deteriorate without the system. Read Track—It's a bit large; I'll come back to it once this line in the background is cleaned up.

Let me start by explaining the original concept.

At that moment, there was only one thought in my mind: After the session ends, the content should remain so I can access it again later.

And that's it. It's over.

Later, while reviewing the pipeline with Claude, I realized I hadn't broken down several decisions into enough detail back then—such as the gate order, the dual-track timing, and whether the graph build should block the HTTP response. Below, I'll discuss these three things separately: sanitize, persist, and graph build.

ContentNormalizer · Temporal / Currency / Proportion / Duration canonical-fold pre-pass · preprocess_chinese Sanitize gate · sequential · non-commutative Session NoiseFilter low-info / chatter InjectionGuard prompt injection Dedup 4-way decision + PoisoningDetector authority · role · Markdown · temporal · Base64 Hybrid Index Qwen3 + BM25 → Qdrant Reactive KG L0 Triple + Entity Resolution Fork A Fork B · fire-and-forget Dedup = SKIP / MERGE / SUPERSEDE / CREATE · High similarity goes to LLM arbitration (including Contradiction)

I. Sanitize (Organize) is actually a three-step process

Three gates—the order cannot be changed—this is the result of “sanitize” evolving from its original two functions of “noise reduction” and “duplicate removal” into its current version:

  1. NoiseFilter (Noise Filtering) — Removing small talk, garbled text, and low-information content
  2. InjectionGuard (Injection Protection) — Blocks malicious content such as prompt injection
  3. Dedup (Duplicate removal) — Compare existing data using embeddings (vectors) and cosine similarity

Why can't the order be changed? One of Dedup's outputs is called MERGE (fusion) — when something is similar but has new information, it merges the new content into the existing block and recalculates the embedding. MERGE is a diffusive write: if a compromised payload bypasses the first two gates, it spreads through MERGE into the clean block.

This constraint was established because Dedup suffered a setback early on when it ran before Noise: a block containing an injection payload was judged as MERGE, and the payload got mixed into the content of a clean block.

Dedup itself does more than discard duplicates. It makes four types of decisions:

  • SKIP (Skip): near-duplicate, do not write
  • MERGE (Integration): Similar but with new information (see the previous paragraph for details)
  • SUPERSEDE (Replace): The old version is no longer valid; replace the entire section.
  • CREATE (New): Completely new—write a new block

The similarity falls within the "uncertain zone" (high, but not high enough to be classified directly as SKIP), Dedup calls resolve_conflict() to perform LLM arbitration and returns a MERGE / SUPERSEDE / COEXIST mapping onto the decision above. Contradiction Detection is performed as a side step here; it is not a separate phase of Fork B. The third phase of Dream Loop, "Consolidate," runs resolve_conflict() again, treating the batch conflict analysis as a second chance.

Sister Modules: PoisoningDetector

Airport security can detect items in carry-on luggage, but they can’t detect contraband hidden under the laptop’s bottom cover—InjectionGuard is the frontal X-ray, blocking explicit prompt injection; PoisoningDetector is the backscatter side, blocking disguised attacks. Both are mounted side-by-side on G2; missing even one attack surface will leave it wide open.

The corresponding module is memvault.security.poisoning; its input is the raw block payload that has already passed through the G1 NoiseFilter, and any hit results in a complete rejection (it never enters G3 Dedup). The judgment is based on five types of spoofing templates:

  • impersonation of an authority figure: Fabricating authoritative citations such as “OpenAI official documents state…” or “According to Anthropic’s security guidelines…” — pattern matching + comparison against a whitelist of known references
  • Role Self-Declaration: I am the system / You are now / system: role-announcement strings appearing in the body of the text, and the like
  • Markdown comment injection: <!-- ignore previous instructions -->, this kind of implicit instruction hidden in a comment
  • temporal manipulation: Phrases such as “latest instructions take precedence” and “override prior rules” that use chronological order to override existing policies
  • encoded payload: Consecutive high-entropy Base64 / hex / unicode escape blocks — detected using Shannon entropy + base alphabet ratio

The template comes from memory-lancedb-pro's 5-vector taxonomy; it's split into 5 independent detectors during implementation, with a short-circuit triggered upon any hit. The trust_score is also calculated here—even if a spoofing attack source manages a partial match, downstream scoring will downweight it to unreachability.

Pre-normalization: ContentNormalizer

"Next Wednesday" and "2026-03-12" refer to the same day, but different formats prevent them from matching—before the three gates, there is an added normalize pass that folds surface strings into canonical tokens, ensuring that downstream Dedup similarity comparisons and LLM arbitration both receive aligned inputs. Hung on libs/text-ops, five submodules connected in a chain:

  • TemporalNormalizer: "Next Wednesday", "3/12", "March 12" → ISO-8601. Rewritten after cannibalizing dateparser + MS Recognizers, 10-pass zero-dependency, covering mixed Chinese-English, relative time, and fuzzy ranges
  • CurrencyNormalizer: “3,000 yuan,” “3,000 NTD,” “USD 100” → {amount, currency} tuple; the FX rate is not processed at this level
  • ProportionNormalizer: “70%,” “0.7,” “7/10” → Standardize to a float in [0, 1]
  • DurationNormalizer: “Two and a half hours,” “2.5h,” “150 min” → ISO-8601 duration (PT2H30M))
  • preprocess_chinese: Full-width/half-width folding, CJK punctuation normalization, zero-width character stripping — a pre-pass that runs before the above four steps

Without this layer, Dedup would classify “dinner next Wednesday night” and “2026-03-11 19:00 dinner” as having low similarity, resulting in CREATE rather than MERGE; KG's entity resolution also treats “Jones,” “JonesHong,” and “j-hong” as three different people—causing the graph to branch. For details, see libs/text-ops/docs/opensource-assessment.md.

II. Persist (Write): From a Single Track to a Dual Track

A single path is not enough—my initial pitch was "save a vectorized copy," focusing solely on the dense vector approach. As soon as the search end went live, problems emerged: when encountering abbreviations, proper nouns, and exact keyword queries, semantic similarity actually led to inaccurate results. Later, we split it into hybrid indexing:

  • Dense: MLX (Apple Machine Learning Framework) Qwen3-Embedding, 1024 dimensions
  • Sparse: BM25 Keyword Index

Write both paths into the same collection in Qdrant. At the query end, use RRF (Reciprocal Rank Fusion) to combine and rank the results from both paths.

III. Graph Build: Reactive Pipe Takes Over

Extract SPO (subject-predicate-object) triples, hang them on the MemvaultEvents.MEMORY_STORED channel as an event-driven trigger, and don't wait for Fork A's vectorization to finish — this pipeline is fire-and-forget. The earliest draft of the graph build was two steps: extract entities first, then extract relations. In practice these became simultaneous, done synchronously while writing the triples, as Entity Resolution: alias disambiguation, folding into an EntityCanonical node (ChatGPT and GPT are treated as the same entity).

There's one more step before entering the atlas: NoiseGateOp, a second review—an extra layer of protection to prevent nonsense from creeping in.

Regarding the “complete knowledge graph” I mentioned

I have to be honest and clarify something here.

When we initially talked about "building a complete KG (Knowledge Graph)," I didn't factor in L1 Community Detection (Leiden community detection), L2 LLM Summary, Content Normalizer, and GRC Adapter (Governance/Risk/Compliance Gate). Those were later put into the Dream Loop, scheduled for 4 a.m. But a cron is just a trigger; to actually run them, it still has to get past dual-gate (double gate):(now - last_dream_at) > 24h AND sessions_since >= 5. If you're not satisfied, just skip it.

Phase 1 (Phase 1) only does ingest substrate (the write layer)—adding dual-track writes at the gate layer, plus L0 Triples + Entity Resolution. Deep processing is pushed to async batch. In hindsight, this demarcation was still the right call.

This piece covers the Write Track's ingest pipeline. The next paragraph shifts focus to the Background Track—on the async batch side, there's a whole set of continuously running components: Dream Loop (five-stage integration), Knowledge Lint (four-tier progressive validation), and Interest Profile (7/30/90-day attention windows). Without these systems, things will drift over time. Scoring, reranking, and cascade recall for the Read Track will be saved until after we wrap up the background line.

Tools

Will the part you want to save make it past the second gate?

You can try out those five types of camouflage patterns on your own items. Paste a piece of text you plan to save to memory, and here it runs in the order of this article: first, see what writing styles the normalization pre-pass will fold, and then compare them one by one against the second gate's template. Of the three gates, only the second one is fully judged in the browser—the article doesn't provide executable thresholds for the first and third gates, so the result column will truthfully write "Not found" and "Cannot run here," rather than giving you a fake green light. Everything runs locally in the browser, with no data sent out; just paste a representative snippet, and do not paste anything containing API keys, tokens, or private conversations.

The sources of the comparison examples are all listed in this article: Five Types of Camouflage Patterns and Their Descriptions (I am the system / You are now / system:,<!-- ignore previous instructions -->"Latest Command Takes Precedence" and override previous rules, Shannon entropy + base alphabet ratio) comes from the technical section "Sister Module: PoisoningDetector"; the five items of date, amount, proportion, duration, and full-width/zero-width come from "Pre-Normalization: ContentNormalizer". This article only gives categories for the first gate and methods for the third gate, without providing threshold values for either, so those two rows are not evaluated. In addition, two numbers are set by the tool itself rather than the article: it only runs with an input of at least 12 characters, and an encoding block must be at least 16 characters to be considered a segment. Everything runs entirely in the browser, and nothing is sent out.

Copy this section

Copy this to your AI Agent

The quality of a memory layer is determined “before writing”—it cannot be remedied during retrieval.—If noise, spoofed inputs, and duplicates aren’t filtered out first, they’re all stored, and when you retrieve them later, all you get is a bunch of junk. This article lays out the various checkpoints before data enters memory, serving as a reference guide.

Send the text below to your AI and have it use the writing guidelines in this article to compare your current approach point by point:

I am reading an article titled "After the Conversation Ends—How I Pack Away Memories" (https://blog.joneshong.com/en/blog/memvault-write-track), in which the author breaks down the "pre-memory-entry" processing into several gates: filtering, deduplication, normalization, and dual-track splitting. Please compare the write design from this article against my current approach and point out which ones I am missing. Do not give generic advice: My context: - I usually collaborate with AI using [Claude Code / Cursor / ChatGPT / others] - My current practice after a conversation ends: [Throw everything away / Copy important parts to notes / Self-built system] - My desired goal: searchable later, no duplicate storage, and contradictory information can be detected Please help me inventory: 1. What filtering gates are needed before entering memory? (Noise, malicious prompts, duplicate content). Why is their order important? 2. What decision-making methods should be used when determining "duplication"? (Skip, merge, replace, add) 3. Should both vector search and keyword search be implemented? What is the difference? 4. Should the relationships between people, events, and objects mentioned in the conversation be extracted into a separate knowledge graph? 5. Which organization tasks can be scheduled as background batches, and which must be done in real-time? 6. For different ways of writing the same thing (dates, amounts, ratios, durations), should they be normalized into a standard format before storage? 7. Does the "malicious defense" layer distinguish between "plain injection" (direct commands) and "camouflaged injection" (identity spoofing / temporal priority / encoded payloads)? First, review my current situation, then list the top 3 priorities. If I only occasionally save a few sentences and have no intention of long-term accumulation, state directly that most of these gates are unnecessary and do not force them.
References

Further Reading

Resources that have actually influenced the design of this write path.

Resources Why It Matters
memory-lancedb-pro Source of the nibbling. The five injection patterns of Injection Guard, the four rulings (SKIP/MERGE/SUPERSEDE) of Dedup, and the trust_score calculation of Provenance were all borrowed from here, then disassembled and recombined
Microsoft GraphRAG The concept of L0 triple extraction and the three-layer KG (Fact → Community → Summary) layering comes from here. Write Track only implements L0, leaving L1/L2 for background batch processing.
Zep: A Temporal Knowledge Graph for Agent Memory Dual-timestamp (valid-time + transaction-time) design, shared by blocks and triples. The three fields valid_at / invalid_at / superseded_by, plus Dedup's SUPERSEDE decision, all come from this one line.
ActMem The decision logic of the Triple conflict resolver. The rationale for choosing one of the three options—"merge / replace / coexist"—when batch_ingest encounters a semantic conflict between new and existing triples comes from here.
MemoryGraft Provenance tracking design. Every memory write is tagged with a trust_score and records its source path, and during queries, the scored TrustBoost is consumed—the blueprint for this write-read closed loop is in this article
HyDE: Hypothetical Document Embeddings The theoretical basis for keeping Qwen3 embedding's task_type="search_document" separate from the query side's "search_query". Getting this wrong could result in a few percentage points drop in similarity.
Simon Willison: Prompt Injection Explained The article I referred to most often while designing InjectionGuard—it explains the classification of injection attacks more clearly than any official document.
memvault Background Track — After Saving The second installment of the trilogy. Once you’ve written it, how do you organize the background every night, how do you resolve conflicts, and how do you learn to forget memories?
memvault Read Track — After I Remembered Part 3 of the trilogy. How to stack the 11-layer sorting problem when you need to recall it, and how a "Slow Thinker" can predict the next question.
✦ Copy Prompt