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 conversationlastBlockValues: A snapshot of every block's value from the previous sync operationlastSeenMessageId: The most recent assistant message already displayed to the userconversationId: 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/claudestate folder. When multiple projects set this to the same path, they share the sameconversations.jsonand session files. - LETTA_MODE: Determines whether the agent injects full memory blocks (
full) or only messages (whisper), as implemented in the configuration loader withinscripts/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_HOMEenvironment variable enables multiple projects to access the same.letta/claudestate folder. - Global Mapping:
conversations.jsonmaintains the session-to-conversation mapping that constitutes the single agent brain. - Session Isolation: Individual
session-<id>.jsonfiles 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.tsorchestrates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →