root@kaus.com:~/projects$ cat lifeline.md
Lifeline
activePersonal
Kbit · page guide
1 / 4
Use the section map on long studies.
Jump between methodology, results, experiments, limitations, and related writing without losing your place.
2 / 4
The narrative carries the claim.
Read each section as problem, method, evidence, and boundary. Negative results remain part of the story.
3 / 4
Inspect figures alongside the narrative.
Read figures in place, scroll wide charts or tables horizontally, and follow any source links for the underlying evidence.
4 / 4
Keep exploring the corpus.
A personal-assistant prototype that extracts memories from conversations and selects context for later prompts using similarity, importance, and recency.
I built a React and Django assistant with persistent memory, prompt inspection, and a separate LangGraph path for Gmail tools. Ordinary chat selects stored context before calling the language model, then starts memory extraction from the completed conversation pair. Historically deployed on EC2 with nginx, systemd, and SSL through GitHub Actions. Current hosted availability and runtime quality have not been independently verified.
Technologies
- Python
- Django
- React
- LangGraph
- OpenAI API
- AWS EC2
Skills
- Agent systems
- Tool use
- RAG / semantic memory
- Full-stack development
- Observability
Links
Related projects
- Ordinary chat: up to 5 scored memories plus 3 from the conversation
- Similarity threshold 0.3, followed by importance and recency weighting
- 16 fictional retrieval cases; production quality unverified
Remembering is a selection problem
A conversation can contain information that matters much later. Keeping the transcript is straightforward. Deciding which part should enter a new prompt is harder: a recent remark may be irrelevant, a durable preference may be old, and a similar sentence may concern the wrong situation. Lifeline makes that selection an explicit part of an ordinary chat request.
I built the assistant around stored memories rather than expecting the model to carry context between requests. The implementation separates extracting a candidate memory, storing it with its source, retrieving candidates for a new message, and formatting the selected context. That separation makes the mechanism inspectable. It does not establish that the model extracted the right facts or that using them improves an answer.
Two passes through a conversation
The React interface sends a message to Django. Before an ordinary model call, the view retrieves memories for the requesting user, combines them with memories associated with the current conversation, and builds an enhanced prompt. Recent messages provide immediate conversational context alongside that stored memory. The ordinary text path sends this assembled prompt to the model.
After a response has been saved, the view starts a background thread to extract memory from the user message and assistant response together. The extractor asks an LLM whether the pair contains something worth retaining. If it returns a memory, the backend embeds its content and creates a Memory record linked to the user, source conversation, and source message. This is a later write, not something the current response waits on.
The timing matters. An answer can already exist while extraction is unfinished or fails. The thread is not a durable job queue, and the extraction function logs an exception and returns no memory. To inspect that separation, I exercised the original Django handler against a fresh SQLite database and held its scheduled callback until after the API response was rendered. Releasing the callback then saved a memory that a new conversation retrieved. The ordering is controlled by the probe, not measured production timing.
This isolated backend run uses fictional users, fixed provider replies and a fresh SQLite database. The probe holds the scheduled extraction callback until the first API response is rendered.
01 · Reply saved, callback held
Rowan says:
“For this fictional workshop I prefer diagrams in tutorials.”
2 messages saved · 0 memories · response 201
02 · Run the extraction callback
The original parser accepts the stipulated extraction JSON and saves this memory:
“Fictional Rowan prefers diagrams for workshop tutorials.”
Memory 1 · source conversation 1 · assistant message 2
03 · Ask in a new conversation
“How should we prepare the workshop?”
The actual prompt contains the saved preference. Semantic retrieval supplied 1 memory; the new conversation supplied 0 conversation-specific memories.
Read the later conversation's captured prompt
You are LifeLine, a smart AI assistant that knows the user well and provides helpful, contextual responses. Core Principles: - Be concise but warm - no unnecessary verbosity - Use memories naturally without explicitly stating "I remember" - Infer context from past conversations and user patterns - Provide direct answers first, then elaborate if needed - Adapt to the user's communication style and preferences Memory Usage: - Seamlessly integrate what you know about the user - Reference past topics, preferences, and goals naturally - Build on previous conversations without over-explaining - Use context to provide more relevant, personalized responses Response Style: - Direct and helpful, not chatty - Use markdown for clarity (headers, lists, emphasis) - Anticipate follow-up needs - Be proactive with suggestions based on user patterns Example approach: - Instead of: "I remember you mentioned you like coffee. Would you like me to help you find coffee shops?" - Say: "Here are some highly-rated coffee shops near your office in downtown SD." Be smart, contextual, and efficient. User: Rowan Context about Rowan: • Workshop preference: Fictional Rowan prefers diagrams for workshop tutorials. User: How should we prepare the workshop?
When the memory write fails
Two further requests received their fixed replies before the probe injected a failure into the queued extraction work.
- Extraction call raises
- Response 201; 2 messages remain; 0 new memories.
- Memory embedding raises
- Response 201; 2 messages remain; 0 new memories.
Jules asked the same question in a separate conversation and received 0 selected memories. In this original run, a request for Rowan's conversation wrote no messages but returned 500, exposing an error-handling defect. These are bounded checks with test authentication.
Following the defect to a repair
A separate regression check on the clean repository base found that notes also needed an ownership check. These two requests used another fictional user's message ID:
- Read another user’s notes
- Before: response 200, notes returned. After: response 404, no persisted changes.
- Add a note to their message
- Before: response 201, 1 note added. After: response 404, no persisted changes.
The repair checks ownership before processing a message or note, returns 404 for foreign or missing objects, and rejects invalid owner content with 400. All 62 request cases pass, including 42 invalid-body cases; the same checks produced 51 failed assertions on the original API. An independent run reproduced the result.
This is local commit faaedc1, tested with fictional users and fixed providers. It has not been merged into the original checkout or deployed as a backend service. Login and hosted access control still need verification.
Store the interpretation with its source
Memory is a Django model with content, a type, tags, an importance score, an embedding, timestamps, and links back to the conversation. Types include preferences, goals, and facts. The conversation-pair extractor also records which messages it examined and whether its output describes an actionable item or deadline. These fields make the stored interpretation easier to inspect than an unexplained vector alone.
There is an important distinction between a remembered statement and a verified fact. The extractor reads the assistant response as well as the user message. An assistant suggestion can therefore contribute to the material being summarized. The extraction prompt and returned confidence are not an independent check that the resulting memory is accurate or appropriate to retain.
Embeddings are stored with each memory in a JSON field. Retrieval embeds the new query and compares it with those stored vectors in Python. There is no vector database in this path. The implementation gives the project a small, inspectable retrieval mechanism, while leaving embedding quality and the correctness of the stored content as separate questions.
Similarity opens the door, then other signals rank
The chat caller requests at most five relevant memories with a minimum cosine similarity of 0.3. The database query first restricts candidates to the requesting user and memories with embeddings. The function then scans those candidates, drops similarities below the threshold, and scores the survivors. A high importance value cannot rescue a memory that fails this similarity filter.
The score gives similarity a weight of 0.6, importance 0.3, and recency 0.1. Recency decreases with whole days since the memory was updated and reaches zero after 30 days. The age comes from updated_at, not the last time retrieval accessed the memory. Reading a memory increments separate access fields, so that accounting should not be described as making its content newly current.
The fictional example below shows why this is different from selecting the nearest vector. A moderately similar, important, recent memory can outrank the closest but older, less important candidate. Those weights encode a retrieval preference. They are implementation choices, not coefficients shown here to improve relevance on a judged dataset.
All seven memories and input values below are fictional. Similarity is stipulated, not measured from embeddings. This demonstrates the retrieval arithmetic only.
score = 0.6 × similarity + 0.3 × importance + 0.1 × recency
Recency is 1 − min(1, age in whole days / 30), using time since the memory was updated. Similarity below 0.3 excludes a row before scoring. The chat caller takes at most five survivors.
| Memory | Similarity | Importance | Age (days) | Recency | Score | Retrieval decision |
|---|---|---|---|---|---|---|
| A: Prefer diagrams in tutorials | 0.90 | 0.20 | 30 | 0.00 | 0.60 | Rank 3, selected |
| B: Prepare a short workshop outline | 0.80 | 0.90 | 3 | 0.90 | 0.84 | Rank 1, selected |
| C: Keep examples runnable | 0.70 | 0.50 | 0 | 1.00 | 0.67 | Rank 2, selected |
| D: Use a small sample project | 0.60 | 0.50 | 15 | 0.50 | 0.56 | Rank 4, selected |
| E: Include a glossary | 0.50 | 0.20 | 0 | 1.00 | 0.46 | Rank 5, selected |
| F: Archive old workshop notes | 0.40 | 0.10 | 30 | 0.00 | 0.27 | Rank 6, outside top five |
| G: Try a new garden layout | 0.29 | 1.00 | 0 | 1.00 | Not scored | Below similarity threshold |
B scores 0.84 and precedes A at 0.60 despite lower similarity. F passes the threshold but falls outside the five selected memories. G is excluded from this branch even with maximum importance and recency. The expected selection is B, C, A, D, E.
This is not final prompt order. Conversation memories enter separately, and the formatter reorders the combined selection by importance and creation time. No answer-quality conclusion follows from these scores.
The retrieval list is not the final prompt order
The view also requests up to three memories from the current conversation, ordered by importance and creation time. That branch does not use the semantic similarity threshold. The two lists are combined and deduplicated by memory ID, so a memory rejected by the similarity branch could still enter through conversation membership. Describing every included memory as a similarity match would misstate the implementation.
The formatter then sorts the combined selection again, by importance and creation time, and includes up to eight entries. It shortens each entry to at most 100 characters. Retrieval score determines one branch's selection, but it is not carried into the formatter as the final ordering rule. Truncation is another transformation: the text the model receives may be shorter than the stored memory that was selected.
Recent message history is also shortened by the prompt builder. Although the caller supplies a history-token parameter, this builder uses a recent-message and character-based policy instead. I would not describe it as enforcing the caller's token budget. To understand an answer, the useful object to inspect is the assembled prompt after these transformations, not just the original retrieval list.
This separate four-memory fixture executes the original request and formatting code with fictional records, embeddings and history. It stops at the model-call boundary, capturing the input without generating an answer.
1. Combine the two selections
- Similarity branch, in score order
- A → C → B
- Current conversation
- D → B
- After removing duplicate IDs
- A → C → B → D
2. Read the memory text sent onward
Exact excerpt from the intercepted ordinary-chat prompt.
- D: Try a new garden layout.
- B: Prepare a short workshop outline.
- C: Keep examples runnable. Use a tiny fictional sample. Use a tiny fictional sample. Use a tiny f...
- A: Prefer diagrams in tutorials.
B appears once after deduplication. D has similarity 0.29, below the 0.3 threshold, but enters through the conversation branch and sorts first on importance. C is shortened to 100 characters, including its title and ellipsis.
Read the exact ordinary-chat prompt
You are LifeLine, a smart AI assistant that knows the user well and provides helpful, contextual responses. Core Principles: - Be concise but warm - no unnecessary verbosity - Use memories naturally without explicitly stating "I remember" - Infer context from past conversations and user patterns - Provide direct answers first, then elaborate if needed - Adapt to the user's communication style and preferences Memory Usage: - Seamlessly integrate what you know about the user - Reference past topics, preferences, and goals naturally - Build on previous conversations without over-explaining - Use context to provide more relevant, personalized responses Response Style: - Direct and helpful, not chatty - Use markdown for clarity (headers, lists, emphasis) - Anticipate follow-up needs - Be proactive with suggestions based on user patterns Example approach: - Instead of: "I remember you mentioned you like coffee. Would you like me to help you find coffee shops?" - Say: "Here are some highly-rated coffee shops near your office in downtown SD." Be smart, contextual, and efficient. User: Alex Context about Alex: • D: Try a new garden layout. • B: Prepare a short workshop outline. • C: Keep examples runnable. Use a tiny fictional sample. Use a tiny fictional sample. Use a tiny f... • A: Prefer diagrams in tutorials. Recent conversation: User: Let us plan a fictional workshop. Assistant: We can start with a small example. User: Help me plan the next workshop.
Two no-memory checks reached an identical prompt: one had no matching memories, the other had an injected embedding failure. Neither had conversation memories. The error log distinguishes them, but the model input does not. The agent-mode check instead received the raw question and history, without this assembled memory prompt.
An inspectable path with real costs
The retrieval scan grows with the number of stored memories for a user. It computes a cosine comparison for each candidate with an embedding, sorts the accepted results, and updates access metadata for selected memories. Limiting the returned list does not remove the scan. At a small scale this keeps the algorithm easy to follow, but the source alone provides no latency or capacity result.
The chat view creates a debug record containing the assembled prompt, selected-context information, model identity, and response metadata. That is a useful inspection surface because it can connect a request with what the ordinary model call actually received. It is not a quality metric. Counts of selected memories do not tell us whether they were relevant, whether an important detail was truncated, or whether the answer used them correctly.
Retrieval logs broad failures and returns an empty list. Continuing without memory may keep a request moving, but an empty result can then mean either that nothing matched or that retrieval failed. Without inspecting that distinction, a plausible answer can hide a broken memory path.
Keep the agent branch separate
Lifeline also implements Gmail tool calling through a LangGraph agent. That is a separate request branch. In the inspected chat view, the agent receives the current question and conversation history, without the enhanced memory prompt built for ordinary chat. Memory-related metadata can still be attached to the response, so its presence does not prove that the agent consumed the selected memories.
Ordinary chat and tool execution therefore need separate evaluation. A debug record can describe context assembled earlier in a request without establishing that a later branch received it. The consumer's actual input is the boundary that matters: in this implementation, the enhanced memory prompt reaches the ordinary text model, while the tool agent starts from the question and recent conversation.
What the evidence establishes
The seven-memory example checks the scoring arithmetic using invented descriptions and stipulated similarities. The separate four-memory trace goes further: it executes the original request, retrieval, deduplication and formatting code against in-memory records, then captures the argument at the model-call boundary. The source hashes, fictional inputs and exact prompt are retained. Neither example calls an embedding model, runs Django or opens a database.
The backend lifecycle probe adds real Django models, SQLite writes, request handling and JSON serialization. Original extraction and retrieval code connects the stored message pair to a memory and a later prompt. Fixed model replies and embeddings make this a wiring check. Injected extraction and memory-embedding failures leave the chat response and its messages intact without adding a memory. Another fictional user's query omits the saved preference, but the original foreign-conversation request returns a server error.
Following that defect exposed a missing ownership check on notes. A separate local repair scopes both message and note access to the conversation owner and validates content before processing. A 62-case Django/SQLite regression check, including 42 invalid-body cases, passes and was independently repeated. The same checks produce 51 failed assertions on the original API. The repair is committed on an isolated branch; it has not been merged into the original checkout or deployed as a backend service.
The historical EC2 deployment does not establish current hosted availability. This probe also leaves login, migration history, production thread scheduling and durable background work untested. The inspected source has local changes, so file hashes identify the exercised bytes separately from the base commit. The follow-up below adds a small judged retrieval comparison with local surrogate embeddings. Actual-provider retrieval, generated answers and current hosted behavior remain unverified. The useful boundary is still both ends of the path: whether extraction saved context, and which version reached the later consumer.
Experiments
1 experiment write-up · continued development log
Retrieval weights and the context that survives · 2026-10 · completed
Setup
The earlier examples established how the memory path works, but their similarity scores were stipulated. This follow-up used real text embeddings from a pinned local MiniLM model on 16 fictional questions, each with eight candidate memories. A separate Astra reviewer labeled relevance without seeing importance scores, ranking methods or outputs. Twelve questions had recorded answers and four did not. The small authored corpus and model-based judgments make this exploratory evidence.
The original retrieval function was compared with cosine-only selection using identical vectors, candidate pools, a fixed clock, the existing 0.3 threshold and a five-memory limit. Both selections then passed through the original formatter. The local model is a surrogate for Lifeline's configured embedding provider, so its similarity threshold is not a calibrated production setting. No application service, real user data or answer-generation model was used.
Result
Both methods selected all 15 essential memories across the 12 answerable questions, and both had 28.75% mean precision among returned memories. The weighting rule changed which distractors were included, but did not improve either selection measure. Only six of those questions had more than five eligible candidates, which limits how much this comparison can say about selection under pressure.
Cosine-only ranking placed useful memories earlier more often on this corpus. Its mean nDCG@5 was 0.879 versus 0.769 for the weighted rule. This is retrieval order before the shared formatter sorts again, and it does not establish a production winner. After formatting, both methods retained only 7 of the 15 exact supporting quotes. A partial quote can still convey some meaning, but two long notes lost the entire answer-bearing clause despite being retrieved.
An exploratory comparison used 16 fictional queries and 128 memories, with sentence-transformers/all-MiniLM-L6-v2 as a local surrogate embedding model. The reviewed set contains 12 answerable queries and 4 with no answer in their memories. Both rankings use the same embeddings, a 0.3 similarity threshold, and at most 5 results.
| Measure | Original weighted | Cosine only |
|---|---|---|
| Essential memories selectedPooled count across answerable queries | 15/15 | 15/15 |
| Mean selection precisionRelevant share of returned memories, averaged per answerable query | 28.75% | 28.75% |
| Mean nDCG@5Graded ranking quality before formatting, averaged per answerable query | 0.769 | 0.879 |
| Exact supporting quotes retainedPooled count after the actual formatter | 7/15 | 7/15 |
Only 6/12 answerable queries had more than 5 eligible memories, so only those could change the selected set. The two rankings selected the same sets in 9/16 cases and produced identical formatted context in 9/16. This study does not establish a ranking improvement.
A stored answer lost during formatting
Who has the lighthouse jigsaw I lent out?
Both rankings selected this memory. The underlined text contains its labeled answer.
Stored memory
At the puzzle evening we cleared the round table, sorted edge pieces into shallow trays, and talked about swapping boxes once everyone had finished their current scene. The mountain puzzle went back into storage, and we counted the spare sorting trays before closing. I lent the lighthouse jigsaw to the Riverbend Craft Circle.
Actual formatted entry
Jigsaw evening notes: At the puzzle evening we cleared the round table, sorted edge pieces into s...
The quote check requires the complete labeled text to survive. It is stricter than semantic retention: a shortened passage might still convey part of a fact. No answers were generated or graded, and these surrogate results do not measure production quality.
Methods and ranking detail
Astra authored the fictional cases; a separate model reviewed their labels blind to ranking outcomes. The frozen run executed the original retrieval and formatting functions in isolation, with fake storage and a fixed clock. The baseline changed only the ranking score to cosine similarity.
Before formatting, the weighted ranking had 2 nDCG wins, 4 ties, and 6 losses against cosine only on the 12 answerable queries. nDCG rewards relevant memories near the top; the formatter can subsequently change that order and shorten the text.
Model revision: 1110a243fdf4706b3f48f1d95db1a4f5529b4d41
What it changed
The result makes the next useful check more specific: preserve answer-bearing content through formatting before treating a ranking adjustment as a quality improvement. The borrowed-jigsaw example shows the failure directly. The correct note reaches selection, then its introductory text consumes the character allowance and the name of the borrower disappears.
The application and its weights remain unchanged. A useful next comparison would test a different context budget or formatting policy alongside selection, then evaluate generated answers using the intended embedding provider and new cases. The four questions without recorded answers all returned some context, including notes explaining uncertainty. Without answer generation, that observation says nothing about whether the assistant would invent an answer.
notes
- This page includes source traces and an isolated Django/SQLite memory-lifecycle probe with fictional data and fixed providers. Live chat, production scheduling, authentication and answer quality remain unverified.
- The inspected September 2026 source snapshot includes local changes to the chat view. File hashes identify those bytes separately from the repository commit.