{"slug": "how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations", "title": "How I built Mind Palace, my personal memory storage for coding conversations", "summary": "A developer built Mind Palace, a local-first desktop memory workspace that captures and retrieves coding-agent conversations so past decisions and context can be found again. The app pairs an Angular 22.2.1 interface with a Tauri 2.12.1/Rust native layer that supervises bundled Python 3.12.10 executables for transcript validation, vault writes and keyword-based local search, storing sessions as Markdown files, JSON metadata and original sources rather than the SQLite/FTS5 setup explored during research. Ask returns stored passages by keyword instead of generating LLM answers, avoiding API keys and keeping conversations on-device.", "body_md": "*Updated 10 October 2026. This describes the current Windows development build.*\n\nWhen I work with a coding agent, the conversation holds more than the code. It holds the reasons behind the code. Why a stack was chosen. What we tried. What did not work. What should happen next.\n\nI wanted a place to keep that context and find it again when I need it. That is what I am building with **Mind Palace**: a personal memory workspace that stores coding conversations on my device.\n\nThe larger idea includes decisions, preferences and handoffs. The working flow today is more focused: capture Codex conversations, store the original source, read sessions and ask questions that return passages from those sessions.\n\nIn this article, I will walk through the stack, the user workflow, the work happening behind the UI and the challenges that shaped the implementation. The code snippets are excerpts from the project, rather than a separate demo implementation.\n\nTake a question like “Why did we choose SQLite?”\n\nThe answer might already be somewhere in an earlier coding conversation. I want to find that message and open the conversation around it. I also want to know whether it was a suggestion or something I actually agreed to.\n\nThat led to two rules for the storage:\n\nFor now, Ask finds stored passages using keywords. It does not generate a new answer with an LLM. This gives me a usable capture-and-retrieval path without needing an API key or sending my conversations to another service.\n\nThe main stack is **Angular, Tauri and Python**. Each layer has a specific job.\n\nAngular handles Connections, the session Library and Ask. I wanted one place for the screens and the state that connects them. The project uses Angular **22.2.1** and TypeScript.\n\nThe interface tracks whether the vault is connected, whether an operation is running, and what error or result should be shown. For example, these are actual state declarations from `local-vault.ts`:\n\n```\nreadonly connected = signal(false);\nreadonly busy = signal(false);\nreadonly error = signal('');\nreadonly notice = signal('');\n```\n\nThese values let the UI reflect the current operation. An open vault, an import in progress and a failed sync are different states. I do not want the screen to hide that difference. Angular's [signals documentation](https://angular.dev/guide/signals) explains the reactive state mechanism used here.\n\nThis needs to work with local files and local processes, so I chose a desktop application. Tauri connects the Angular interface to a Rust layer. The project pins Tauri **2.12.1**.\n\nRust owns the worker processes and exposes specific commands such as opening the vault, importing captures and reading a session. The interface does not get a general command for running arbitrary executables.\n\nThere is a cost to this choice: I now have boundaries between TypeScript, Rust and Python to maintain. The project uses generated contracts and response validation so those boundaries are checked instead of relying only on matching field names.\n\nTauri documents [embedding external binaries](https://v2.tauri.app/develop/sidecar/). In this project, the native layer supervises prepared Python executables through private process transport; it does not expose the documentation's generic shell example to the frontend.\n\nPython handles transcript validation, the capture inbox, vault writes and local search. The development runtime recorded for this project is **Python 3.12.10**, with a frozen executable used in native checks.\n\nThis keeps the file and text processing together. Bundling it also means the prepared native workflow does not depend on a user manually running a Python service.\n\nThe current authoritative vault uses Markdown session files, JSON metadata and original source files. SQLite and FTS5 were explored during runtime research, but they are not the storage or search engine behind this current flow. The SQLite question in the demo is a fictional conversation topic.\n\nThat distinction matters when explaining the stack. A technology mentioned inside a saved conversation is not necessarily a technology used by the app.\n\nHere is how I expect someone to use the current desktop build.\n\n*This is the complete path from setup to finding a stored memory. Older history has a separate confirmation step.*\n\n**1. Create or open the local vault.**\n\nThe current app uses one default vault under the operating system's local-data folder, at `dev.mindpalace.local/vault`. Custom vault-folder selection is still pending. Once created, the same vault can be reopened.\n\n**2. Set the Codex capture scope.**\n\nIn Connections, select Codex and enter the absolute folder containing its session transcripts. The tested default layout is `%USERPROFILE%/.codex/sessions`; a custom `CODEX_HOME` changes that location. Enable hooks and save the scope.\n\nThe app needs explicit access to that source folder. It does not silently import all older conversations.\n\n**3. Install and review the hook.**\n\nMind Palace merges its handlers into the user-level hook file, preserves other handlers and keeps a backup. Then open `/hooks` in Codex and review the exact commands before trusting them.\n\nInstalling the file and approving the handler are separate steps. The app's setup screen can show installation state, but it does not claim that the current Codex runtime has approved it.\n\n*The Connections checklist in the native development build. This screenshot uses an isolated test setup; an installed hook does not prove runtime approval or delivery.*\n\n**4. Work with Codex.**\n\nAfter approval, start or resume a session. The lifecycle hooks invoke the local receiver. Mind Palace does not have to remain open for this capture step.\n\n**5. Open Mind Palace and sync.**\n\nWhen the vault opens, the app checks available captures and imports verified sessions. There is also a Sync action, and Ask checks for new captures before searching.\n\n**6. Read the session or ask a question.**\n\nIn Sessions, select a conversation and read its messages. Use “Show complete source” when you need the complete stored record. In Ask, use words from the conversation. A matching result includes the stored passage and a source reference.\n\nFor existing history, there is a separate preview-and-confirm import flow. That lets the user check the source, count and size before importing older sessions.\n\n*The Library keeps the saved session and its original messages together. This is a real native screenshot with fictional conversation content.*\n\nI did not want capture to depend on the desktop UI being open. The receiver and the vault worker therefore have different responsibilities.\n\nThe hook receiver validates the event, reads the allowed transcript and writes a unique spool record to the local inbox. Later, the app indexes those records and imports them into the vault.\n\n*The receiver can run while Mind Palace is closed. Indexing and vault import happen later, when the app checks captures.*\n\nThis excerpt from `session_capture.py` shows how the receiver identifies the transcript revision and the session:\n\n```\nsha = hashlib.sha256(data).hexdigest()\nkey = hashlib.sha256((config[\"provider\"] + \"\\0\" + session).encode()).hexdigest()\n```\n\n`sha` identifies the captured bytes. `key` identifies the provider and session together. These are different identities: one conversation can have several source revisions as it grows.\n\nThe receiver writes a unique temporary file, flushes it and publishes the spool record. It avoids the app's shared operation lock. Indexing and deduplication happen later, outside the short synchronous hook path.\n\nThe capture inbox is separate from the vault, under `%LOCALAPPDATA%/MindPalace/capture`. That separation lets capture continue while the vault is closed.\n\nThere are two parts here: the supervised vault worker and the operations triggered by the interface.\n\nWhen the vault opens, the native layer starts the local worker. The worker serves validated requests for storage and retrieval. Opening the vault also checks the recovery journal for interrupted writes. Closing the vault or exiting the app stops the owned vault worker.\n\nThe current sync is **triggered by actions**, rather than a timer that constantly polls. The local-vault service checks captures when opening the vault, refreshing a connected vault, using Sync, or submitting an Ask question.\n\n*The worker serves requests while the vault is open. Sync runs through these stages when triggered; it does not continuously repeat them.*\n\nThe stages inside a sync are:\n\n| Stage | What happens | \n|---|---|\n| Check configuration | Confirm whether Codex capture is configured. | \n| Index the inbox | Validate spool deliveries, deduplicate them and publish source manifests. This also performs approved-root recovery for known sessions. | \n| Import pages | Create or update canonical sessions through the vault writer. | \n| Acknowledge and clean up | After import acknowledgment, evaluate redundant prefixes for safe retention cleanup. | \n| Refresh the Library | Load the updated session list. | \n\nThe import loop in `local-vault.ts` makes the page boundary explicit:\n\n``` js\nconst result = await this.checked<CaptureImportResult>(\n  'capture_import',\n  { offset },\n  validateCaptureImportResult,\n);\nchanged += result.created + result.updated;\nif (result.next_offset === null) {\n  const cleanup: unknown = await this.invoke('capture_cleanup');\n  if (!validateCaptureCleanupResult(cleanup)) throw { code: 'INVALID_RESPONSE' };\n  return changed;\n}\nif (result.next_offset <= offset) throw { code: 'INVALID_RESPONSE' };\noffset = result.next_offset;\n```\n\nThis is an excerpt inside the bounded loop, not a standalone function. Each response is validated. The offset must move forward. Cleanup runs after the final import page.\n\nThe Codex hook can still run while the app is open, because its lifetime belongs to the coding client. There is no background model generating summaries in this flow.\n\nA transcript changes as new turns arrive. Importing every captured revision as a new session would fill the Library with duplicates.\n\nThe solution was a canonical identity based on provider and session. Later revisions update that same session, while selected original source revisions remain separate immutable files.\n\nThere was also a stale-prefix bug: an older, smaller capture could be offered when the latest revision exceeded the earlier import limit. The implementation first refused that incomplete result. Segmented source handling then allowed complete imports up to the current **20 MiB** source bound.\n\nInbox chunks target **64 KiB**, with boundaries after complete JSONL records. The manifest preserves their order so the source can be reconstructed without pretending a truncated prefix is complete.\n\nStoring a source is only useful if I can tell when it has changed unexpectedly. The vault records its hash and byte count, then checks both before returning it.\n\nFrom `Vault.read_session`:\n\n```\nif file_hash(source) != source_meta[\"sha256\"] or source.stat().st_size != source_meta[\"bytes\"]:\n    raise WorkerError(\"CONFLICT\", \"Original source changed; it was not overwritten.\")\n```\n\nThis detects a mismatch with the saved metadata. It does not prove that the conversation itself is correct. It keeps the original record inspectable and avoids silently replacing it.\n\nA process can stop after some files have been written. A timeout also does not always mean that nothing was saved.\n\nThe vault uses a journal to record the intended changes. Recovery checks the target files before completing a transaction. If it finds unexpected changes, it preserves them and reports a conflict.\n\nAn explicit retry uses the same operation ID and the same request. The journal handles that case like this:\n\n```\nif path.exists():\n    intent = self.apply(path)\n    if intent[\"fingerprint\"] != fingerprint:\n        raise WorkerError(\"CONFLICT\", \"Operation identifier was already used.\")\n    return intent[\"result\"]\n```\n\nReusing the ID with a different request is rejected. Retrying the unchanged request can recover the stored result instead of creating another session.\n\nFor the first working version, I kept retrieval small. Ask removes common question words, keeps distinct search terms and looks for passages containing all retained terms.\n\nHere is the matching condition from `Vault.ask_sessions`:\n\n```\npositions = [match for term in terms if (match := re.search(r\"\\b\" + re.escape(term) + r\"\\b\", passage.text, re.IGNORECASE))]\nif len(positions) != len(terms):\n    continue\n```\n\nIf nothing matches, the result says there is insufficient evidence. A matching result carries the session ID, quote, source offsets and snapshot hash. Ask returns up to three sources.\n\n*Ask returns the original passage and a way to inspect its source. This screenshot uses fictional messages in the real native app; SQLite is the conversation topic, not the current vault backend.*\n\nFor captured Codex sessions, search uses known user and assistant conversation fields. Raw system, developer and tool records are preserved in the complete source, but they are excluded from this conversation-search view.\n\nThis approach has limits. A synonym can miss, and adding too many terms can make a question too restrictive. A matching assistant message can also be a suggestion. The user still needs to inspect the context.\n\nCapture, import and retrieval needed separate checks. Seeing a session in the Library does not prove that every hook event arrived.\n\nKnown-session recovery checks the approved source folder for missed tails. If certain known capture failures occur during Ask, the app can still search already stored sessions and show a warning that new captures were not synced.\n\nPerformance debugging also found expensive full resource hashing in the development build. Optimizing the `sha2` dependency reduced measured per-command verification from roughly **1.7–1.9 seconds to 100–127 ms**, while keeping the complete hash checks.\n\nThose changes improved specific paths. They did not close every reliability question. A controlled two-minute installed-client test recovered the exact source but verified only **18 of 20** expected end-event deliveries. That discrepancy remains open. Earlier Windows timeouts are also retained in the verification records.\n\nGrowing conversations produce redundant prefixes. I wanted retention, but a file being old is not enough reason to remove it.\n\nCleanup considers covered prefixes older than **30 days** only after a verified vault-import acknowledgment and reference checks. Unknown, divergent and unimported copies stay intact. If validation exceeds its supported scan budget, cleanup is deferred.\n\nThis makes cleanup conservative. I would rather keep an extra copy than remove a source that has not reached the vault.\n\nThe Windows development workflow has recorded native checks for closed-app capture, restart import, exact source recovery, Ask and opening the source. A complete **20 MiB** source passed capture, import and read with a matching SHA-256.\n\nThe walkthrough uses fictional conversations in the real native application. Separate installed-client checks exercised Codex CLI **0.160.0** with controlled local replies. Those checks are not evidence of real model quality or guaranteed capture on every machine.\n\nClaude Code end-to-end verification, the public installer, Mac testing and model-generated answers remain unfinished. The browser sample is useful for exploring the interface, but its sample state resets on reload and it cannot open a desktop vault.\n\nThe next work is to investigate the remaining Codex event discrepancy and improve reliability before expanding capture to more clients.\n\nThe project is on [GitHub](https://github.com/ashikvarma11/mind-palace). The [user guide](https://github.com/ashikvarma11/mind-palace/blob/main/docs/user-guide.md) covers the current setup, and the [development guide](https://github.com/ashikvarma11/mind-palace/blob/main/docs/development.md) explains the browser sample and prepared native build.\n\nThe main files behind this article are:\n\nIf you use coding agents, what context do you find yourself searching for again? The reason behind a decision, an earlier fix, or where you left the work? That is the kind of feedback I want for Mind Palace.", "url": "https://wpnews.pro/news/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations", "canonical_source": "https://dev.to/ashikvarma11/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations-4f6h", "published_at": "2026-10-10 07:28:59+00:00", "updated_at": "2026-10-10 07:40:09.723752+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["Mind Palace", "Angular", "Tauri", "Python", "Rust", "TypeScript", "Codex", "SQLite"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations", "markdown": "https://wpnews.pro/news/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations.md", "text": "https://wpnews.pro/news/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations.txt", "jsonld": "https://wpnews.pro/news/how-i-built-mind-palace-my-personal-memory-storage-for-coding-conversations.jsonld"}}