How the Claude Subconscious Plugin Handles Agent ID Changes and Conversation Format Migrations
The Claude Subconscious plugin maintains a persistent JSON mapping that stores both conversation IDs and Letta agent IDs, automatically detecting agent switches and upgrading legacy string-only entries to the new object format without user intervention.
The letta-ai/claude-subconscious plugin ensures continuity across Claude Code sessions by maintaining a durable link between Claude Code session IDs and Letta conversations. To handle agent ID changes and conversation format migrations, the plugin implements a robust state management system that validates agent ownership on every session start and transparently upgrades legacy data schemas when accessing stored mappings.
The Persistent Conversation Mapping
The plugin stores session-to-conversation relationships in .letta/claude/conversations.json within the project workspace. This JSON file acts as the source of truth for mapping Claude Code session IDs to Letta conversation metadata, enabling the plugin to resume previous contexts or create new ones as needed.
Legacy vs. Current Schema Versions
The mapping file supports two distinct entry formats that reflect the plugin's evolution:
- Legacy (pre-v1.3.0):
"session-id": "<conversation-id>"— Stores only the conversation ID as a string, implicitly assuming the associated Letta agent never changes. - Current:
"session-id": { "conversationId": "<cid>", "agentId": "<aid>" }— Stores both the conversation ID and the Letta agent ID, enabling the plugin to detect when users switch between agents.
When loadConversationsMap (defined in scripts/conversation_utils.ts, lines 81-88) reads this file, it returns a dictionary that may contain either format, requiring runtime type checking to determine the appropriate migration path.
Detecting Agent ID Changes at Runtime
When a session starts, the plugin validates that any stored conversation belongs to the currently configured agent. If getAgentId (from scripts/agent_config.ts) returns an ID that differs from the stored agentId, the plugin treats the existing conversation as stale and incompatible with the new configuration.
Invalidating Stale Conversations
In scripts/session_start.ts (lines 31-48), the plugin compares the cached agentId against the resolved current agent. When a mismatch is detected, the plugin:
- Logs the agent change event.
- Deletes the stale entry from the conversations map.
- Invokes
createConversationto generate a fresh Letta conversation for the new agent. - Persists the updated mapping via
saveConversationsMap(lines 99-101 inconversation_utils.ts).
// Load the persistent map
const conversationsMap = loadConversationsMap(hookInput.cwd);
// Resolve the current Letta agent ID
const agentId = await getAgentId(apiKey, log);
// Look for an existing entry for this session
const cached = conversationsMap[hookInput.session_id];
if (cached) {
// Convert legacy string → object if needed
const entry = typeof cached === 'string'
? { conversationId: cached, agentId: null as string | null }
: cached;
// *** Agent‑ID changed? ***
if (entry.agentId && entry.agentId !== agentId) {
log(`Agent ID changed (${entry.agentId} -> ${agentId}), clearing stale conversation`);
delete conversationsMap[hookInput.session_id];
conversationId = await createConversation(apiKey, agentId, log);
}
// *** Legacy entry (no agentId) – upgrade it ***
else if (!entry.agentId) {
log(`Upgrading old format entry (no agentId stored), creating new conversation`);
delete conversationsMap[hookInput.session_id];
conversationId = await createConversation(apiKey, agentId, log);
}
// Re‑use the stored conversation
else {
conversationId = entry.conversationId;
}
} else {
// No entry – start a brand‑new conversation
conversationId = await createConversation(apiKey, agentId, log);
}
// Persist the new or upgraded entry
conversationsMap[hookInput.session_id] = { conversationId, agentId };
saveConversationsMap(hookInput.cwd, conversationsMap);
Source: scripts/session_start.ts – lines 24-52 (excerpt)【/cache/repos/github.com/letta-ai/claude-subconscious/main/scripts/session_start.ts#L24-L52】
Automatic Conversation Format Migration
The plugin handles legacy string entries by detecting the absence of an agentId property. When typeof cached === 'string', the plugin recognizes the entry predates the current schema and requires immediate modernization to prevent data loss or agent misalignment.
Upgrading Legacy Entries on Access
In scripts/conversation_utils.ts, the getOrCreateConversation function (lines 66-74) implements the same migration logic used by other hooks like send_messages_to_letta.ts. Upon detecting a legacy string entry, the plugin:
- Removes the old string entry from the map.
- Creates a new conversation bound to the current agent ID.
- Writes back an object-format entry containing both
conversationIdandagentId.
export async function getOrCreateConversation(
apiKey: string,
agentId: string,
sessionId: string,
cwd: string,
state: SyncState,
log: LogFn = noopLog
): Promise<string> {
// … (state‑based fast path omitted)
const conversationsMap = loadConversationsMap(cwd, log);
const cached = conversationsMap[sessionId];
if (cached) {
const entry = typeof cached === 'string'
? { conversationId: cached, agentId: null as string | null }
: cached;
if (entry.agentId && entry.agentId !== agentId) {
// Agent changed → discard and recreate
delete conversationsMap[sessionId];
const conversationId = await createConversation(apiKey, agentId, log);
conversationsMap[sessionId] = { conversationId, agentId };
saveConversationsMap(cwd, conversationsMap);
return conversationId;
} else if (!entry.agentId) {
// Legacy entry → upgrade
delete conversationsMap[sessionId];
const conversationId = await createConversation(apiKey, agentId, log);
conversationsMap[sessionId] = { conversationId, agentId };
saveConversationsMap(cwd, conversationsMap);
return conversationId;
}
// Valid entry → reuse
return entry.conversationId;
}
// No entry → fresh conversation
const conversationId = await createConversation(apiKey, agentId, log);
conversationsMap[sessionId] = { conversationId, agentId };
saveConversationsMap(cwd, conversationsMap);
return conversationId;
}
Source: conversation_utils.ts – lines 48-84 (excerpt)【/cache/repos/github.com/letta-ai/claude-subconscious/main/scripts/conversation_utils.ts#L48-L84】
Core Implementation Files
The migration and detection logic spans three primary files according to the source code:
scripts/session_start.ts: Entry point for the Claude CodeSessionStarthook; performs the initial agent validation and legacy upgrades before the first message is processed.scripts/conversation_utils.ts: Shared utilities includinggetOrCreateConversation, used by message synchronization hooks to ensure conversation validity across all operations.scripts/agent_config.ts: ProvidesgetAgentId, which resolves the active agent from environment variables, saved configuration, or auto-import; the migration logic depends on this resolved value.
Summary
- The plugin stores conversation mappings in
.letta/claude/conversations.jsonusing a schema that includes bothconversationIdandagentIdto track ownership. - Agent ID changes are detected by comparing stored IDs against the current agent resolved via
getAgentId, triggering automatic conversation recreation when they differ. - Legacy format entries (plain strings without agent IDs) are automatically detected by
typeofchecks and upgraded to the new object format upon access. - Stale conversations are discarded and recreated when agent switches occur, ensuring the Letta subconscious always aligns with the active configuration.
- No manual migration steps are required when upgrading from pre-v1.3.0 versions; the plugin handles schema conversion transparently.
Frequently Asked Questions
What happens when I switch Letta agents in Claude Code?
The plugin detects the change by comparing the stored agentId with the current agent ID returned by getAgentId. When they differ—as implemented in scripts/session_start.ts lines 31-38—the plugin deletes the stale conversation entry from conversations.json, creates a new Letta conversation for the new agent via createConversation, and updates the mapping. This ensures the subconscious context always aligns with the active agent without manual intervention.
How does the plugin handle conversations from older versions?
Legacy entries stored as plain strings (pre-v1.3.0) are detected by checking typeof cached === 'string' or !entry.agentId. When encountered in session_start.ts or conversation_utils.ts, the plugin removes the legacy entry, creates a fresh conversation bound to the current agent, and persists the new object-format entry containing both the conversation ID and agent ID, effectively upgrading the schema on first access.
Where is the conversation mapping stored?
The persistent mapping lives in .letta/claude/conversations.json relative to the current working directory. This file is read by loadConversationsMap and written by saveConversationsMap, both defined in scripts/conversation_utils.ts (lines 81-88 and 99-101). The JSON structure maps Claude Code session IDs to either conversation strings (legacy) or objects containing conversationId and agentId (current).
Is manual migration required when upgrading the plugin?
No. The migration logic runs automatically whenever a session starts or a hook accesses conversation data through getOrCreateConversation. Users upgrading from pre-v1.3.0 do not need to manually edit conversations.json; the plugin handles the string-to-object conversion transparently while preserving conversation continuity where possible.
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 →