cd /news/ai-agents/how-i-built-mind-palace-my-personal-… · home › topics › ai-agents › article
[ARTICLE · art-148647] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

How I built Mind Palace, my personal memory storage for coding conversations

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.

by read12 min views1 publishedOct 10, 2026

Updated 10 October 2026. This describes the current Windows development build.

When 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.

I 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.

The 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.

In 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.

Take a question like “Why did we choose SQLite?”

The 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.

That led to two rules for the storage:

For 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.

The main stack is Angular, Tauri and Python. Each layer has a specific job.

Angular 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.

The 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:

readonly connected = signal(false);
readonly busy = signal(false);
readonly error = signal('');
readonly notice = signal('');

These 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 explains the reactive state mechanism used here.

This 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.

Rust 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.

There 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.

Tauri documents embedding external binaries. 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.

Python 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.

This 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.

The 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.

That distinction matters when explaining the stack. A technology mentioned inside a saved conversation is not necessarily a technology used by the app.

Here is how I expect someone to use the current desktop build.

This is the complete path from setup to finding a stored memory. Older history has a separate confirmation step.

1. Create or open the local vault.

The 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.

2. Set the Codex capture scope.

In 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.

The app needs explicit access to that source folder. It does not silently import all older conversations.

3. Install and review the hook.

Mind 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.

Installing 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.

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.

4. Work with Codex.

After 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.

5. Open Mind Palace and sync.

When 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.

6. Read the session or ask a question.

In 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.

For 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.

The Library keeps the saved session and its original messages together. This is a real native screenshot with fictional conversation content.

I did not want capture to depend on the desktop UI being open. The receiver and the vault worker therefore have different responsibilities.

The 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.

The receiver can run while Mind Palace is closed. Indexing and vault import happen later, when the app checks captures.

This excerpt from session_capture.py shows how the receiver identifies the transcript revision and the session:

sha = hashlib.sha256(data).hexdigest()
key = hashlib.sha256((config["provider"] + "\0" + session).encode()).hexdigest()

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.

The 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.

The capture inbox is separate from the vault, under %LOCALAPPDATA%/MindPalace/capture. That separation lets capture continue while the vault is closed.

There are two parts here: the supervised vault worker and the operations triggered by the interface.

When 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.

The 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.

The worker serves requests while the vault is open. Sync runs through these stages when triggered; it does not continuously repeat them.

The stages inside a sync are:

Stage What happens
Check configuration Confirm whether Codex capture is configured.
Index the inbox Validate spool deliveries, deduplicate them and publish source manifests. This also performs approved-root recovery for known sessions.
Import pages Create or update canonical sessions through the vault writer.
Acknowledge and clean up After import acknowledgment, evaluate redundant prefixes for safe retention cleanup.
Refresh the Library Load the updated session list.

The import loop in local-vault.ts makes the page boundary explicit:

const result = await this.checked<CaptureImportResult>(
  'capture_import',
  { offset },
  validateCaptureImportResult,
);
changed += result.created + result.updated;
if (result.next_offset === null) {
  const cleanup: unknown = await this.invoke('capture_cleanup');
  if (!validateCaptureCleanupResult(cleanup)) throw { code: 'INVALID_RESPONSE' };
  return changed;
}
if (result.next_offset <= offset) throw { code: 'INVALID_RESPONSE' };
offset = result.next_offset;

This 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.

The 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.

A transcript changes as new turns arrive. Importing every captured revision as a new session would fill the Library with duplicates.

The 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.

There 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.

Inbox 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.

Storing 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.

From Vault.read_session:

if file_hash(source) != source_meta["sha256"] or source.stat().st_size != source_meta["bytes"]:
    raise WorkerError("CONFLICT", "Original source changed; it was not overwritten.")

This 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.

A process can stop after some files have been written. A timeout also does not always mean that nothing was saved.

The 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.

An explicit retry uses the same operation ID and the same request. The journal handles that case like this:

if path.exists():
    intent = self.apply(path)
    if intent["fingerprint"] != fingerprint:
        raise WorkerError("CONFLICT", "Operation identifier was already used.")
    return intent["result"]

Reusing the ID with a different request is rejected. Retrying the unchanged request can recover the stored result instead of creating another session.

For 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.

Here is the matching condition from Vault.ask_sessions:

positions = [match for term in terms if (match := re.search(r"\b" + re.escape(term) + r"\b", passage.text, re.IGNORECASE))]
if len(positions) != len(terms):
    continue

If 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.

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.

For 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.

This 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.

Capture, import and retrieval needed separate checks. Seeing a session in the Library does not prove that every hook event arrived.

Known-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.

Performance 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.

Those 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.

Growing conversations produce redundant prefixes. I wanted retention, but a file being old is not enough reason to remove it.

Cleanup 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.

This makes cleanup conservative. I would rather keep an extra copy than remove a source that has not reached the vault.

The 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.

The 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.

Claude 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.

The next work is to investigate the remaining Codex event discrepancy and improve reliability before expanding capture to more clients.

The project is on GitHub. The user guide covers the current setup, and the development guide explains the browser sample and prepared native build.

The main files behind this article are:

If 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.

── more in #ai-agents 4 stories · sorted by recency
── more on @mind palace 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-built-mind-pal…] indexed:0 read:12min 2026-10-10 · —