How Claude Subconscious Manages State Across Multiple Projects with a Single Agent Brain

Claude Subconscious maintains one persistent Letta agent brain across multiple projects by storing durable JSON state in a shared directory managed via LETTA_HOME, allowing any codebase to reference the same conversation ID through a central conversations.json mapping.

The letta-ai/claude-subconscious repository enables Claude Code to share a single Letta agent across disjoint codebases by managing state through a durable filesystem layer. This architecture allows developers to maintain continuity of context and memory even when working across unrelated projects, using a shared agent brain that persists conversation metadata outside of any individual repository.

The Shared State Architecture

Centralized State Directory (LETTA_HOME)

The system establishes a durable state directory through the getDurableStateDir() utility in scripts/conversation_utils.ts. This function selects a base directory from the LETTA_HOME environment variable (falling back to the current working directory) and appends .letta/claude to create the persistence layer.

All files that survive across hook runs live under this folder, including the global conversations.json registry and per-session session-<id>.json files. By configuring LETTA_HOME to point to the same path across multiple projects, unrelated codebases can access identical state.

The Conversation Mapping Brain

The conversations.json file acts as the central brain mapping that connects sessions to persistent conversations. The getConversationsFile() function returns the path to this registry, while loadConversationsMap() and saveConversationsMap() handle reading and writing the mapping structure.

This JSON map stores the relationship between session IDs, conversation IDs, and agent IDs. When getOrCreateConversation() executes during a hook run, it first checks the in-memory SyncState, then falls back to the persisted map. If a stored entry references a different agent ID, the system clears it and creates a fresh conversation via the Letta API, caching the new conversation ID in both the map and the session's sync state.

Per-Session State Isolation

Session-Specific Sync Files

While the conversation mapping is global, per-session state remains isolated through individual JSON files. The getSyncStateFile() function constructs paths like session-<sessionId>.json within the durable state directory. The loadSyncState() and saveSyncState() utilities in scripts/conversation_utils.ts manage these files, ensuring each Claude session maintains its own sync checkpoint without interfering with other sessions.

State Contents and Updates

Each session file stores critical synchronization metadata:

  • lastProcessedIndex: The index of the last memory block injected into the conversation
  • lastBlockValues: A snapshot of every block's value from the previous sync operation
  • lastSeenMessageId: The most recent assistant message already displayed to the user
  • conversationId: The Letta conversation ID bound to this specific session

When saveSyncState() persists updates, it captures the current block payloads and message identifiers, enabling differential syncing on subsequent runs.

Hook Orchestration and State Flow

The scripts/sync_letta_memory.ts script serves as the hook entry point that orchestrates state management across projects. When executed, it receives session_id from Claude's hook input and immediately loads the session-specific sync state via loadSyncState().

If the state lacks a conversationId, the script calls lookupConversation() to read the shared conversations.json mapping. After fetching the Letta agent and retrieving new assistant messages, the hook updates lastBlockValues with current memory block payloads, sets lastSeenMessageId to the newest message identifier, and persists the modified state through saveSyncState(). This cycle ensures that every project hook run contributes to and retrieves from the same persistent conversation thread.

Cross-Project Configuration

Environment Variables

Three key environment variables control the shared brain behavior:

  • LETTA_HOME (or LETTA_PROJECT): Defines the root directory for the .letta/claude state folder. When multiple projects set this to the same path, they share the same conversations.json and session files.
  • LETTA_MODE: Determines whether the agent injects full memory blocks (full) or only messages (whisper), as implemented in the configuration loader within scripts/conversation_utils.ts.
  • LETTA_SDK_TOOLS: Controls the client-side toolset availability (read-only, full, or disabled).

Sharing State Across Projects

To share a single agent brain across distinct repositories, configure the environment before running hooks:


# Project A

export LETTA_HOME=$HOME/.letta_shared
cd /path/to/project-a
npm run claude-hook

# Project B (different repository)

export LETTA_HOME=$HOME/.letta_shared
cd /other/path/project-b
npm run claude-hook

Both executions read from and write to the same conversations.json file, ensuring that the Letta conversation ID remains consistent regardless of which codebase initiated the hook.

Implementation Examples

Initializing the Durable Directory

Before first use, ensure the state directory exists:

import { ensureDurableStateDir } from './conversation_utils.js';

const cwd = process.cwd();
ensureDurableStateDir(cwd);

Loading or Creating a Conversation

Retrieve an existing conversation or establish a new one for the current session:

import {
  loadSyncState,
  saveSyncState,
  getOrCreateConversation,
} from './conversation_utils.js';

async function initializeSession(apiKey: string, agentId: string, sessionId: string, cwd: string) {
  const state = loadSyncState(cwd, sessionId);
  const conversationId = await getOrCreateConversation(
    apiKey,
    agentId,
    sessionId,
    cwd,
    state,
  );
  saveSyncState(cwd, state);
  return conversationId;
}

Updating Per-Session Block Snapshots

After synchronizing memory blocks, persist the current state to prevent redundant injections:

function updateBlockSnapshot(state: SyncState, blocks: MemoryBlock[]) {
  state.lastBlockValues = {};
  for (const b of blocks) {
    state.lastBlockValues[b.label] = b.value;
  }
}

Summary

  • Shared Directory: The LETTA_HOME environment variable enables multiple projects to access the same .letta/claude state folder.
  • Global Mapping: conversations.json maintains the session-to-conversation mapping that constitutes the single agent brain.
  • Session Isolation: Individual session-<id>.json files track per-session synchronization checkpoints without cross-contamination.
  • API Integration: getOrCreateConversation() automatically provisions new Letta conversations when agent mismatches occur.
  • Hook Coordination: sync_letta_memory.ts orchestrates the full lifecycle from state loading through differential updates to persistence.

Frequently Asked Questions

How does Claude Subconscious prevent conversation conflicts between different projects?

The system uses the conversations.json mapping to associate specific session IDs with conversation IDs. When getOrCreateConversation() detects that a stored conversation belongs to a different agent ID than the one currently requested, it automatically invalidates the stale mapping and creates a new conversation. This ensures that while the state directory is shared, each unique session receives the correct conversation context for its associated agent.

What happens if two projects use the same session ID simultaneously?

Both projects would reference the same session-<id>.json file and the same conversation ID within the shared conversations.json mapping. The per-session state would be overwritten by whichever hook runs last, potentially causing synchronization drift. To avoid this, ensure unique session IDs across projects or isolate projects using different LETTA_HOME directories when concurrent isolation is required.

Can I migrate an existing conversation to a new project?

Yes. Since the conversation metadata lives in the shared conversations.json file rather than within any specific project directory, you can point a new project to the same LETTA_HOME directory. The lookupConversation() function in sync_letta_memory.ts will retrieve the existing conversation ID from the global mapping, allowing the new project to continue the established conversation thread seamlessly.

What is the difference between LETTA_HOME and LETTA_PROJECT?

Both variables serve as alternative identifiers for the base directory selection in getDurableStateDir(). The code checks LETTA_HOME first, then falls back to LETTA_PROJECT if the former is undefined. If neither is set, the system uses the current working directory. Setting either variable to a shared path enables the cross-project state management described in the single agent brain architecture.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →