# How the Claude Subconscious Plugin Handles Agent ID Changes and Conversation Format Migrations

> Discover how the Claude Subconscious plugin seamlessly handles agent ID changes and conversation format migrations using a persistent JSON mapping. It automatically detects switches and upgrades data without user intervention.

- Repository: [Letta/claude-subconscious](https://github.com/letta-ai/claude-subconscious)
- Tags: internals
- Published: 2026-03-26

---

**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`](https://github.com/letta-ai/claude-subconscious/blob/main/.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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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:

1. Logs the agent change event.
2. Deletes the stale entry from the conversations map.
3. Invokes `createConversation` to generate a fresh Letta conversation for the new agent.
4. Persists the updated mapping via `saveConversationsMap` (lines 99-101 in [`conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/conversation_utils.ts)).

```typescript
// 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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/send_messages_to_letta.ts). Upon detecting a legacy string entry, the plugin:

1. Removes the old string entry from the map.
2. Creates a new conversation bound to the current agent ID.
3. Writes back an object-format entry containing both `conversationId` and `agentId`.

```typescript
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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/session_start.ts)**: Entry point for the Claude Code `SessionStart` hook; performs the initial agent validation and legacy upgrades before the first message is processed.
- **[`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts)**: Shared utilities including `getOrCreateConversation`, used by message synchronization hooks to ensure conversation validity across all operations.
- **[`scripts/agent_config.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/agent_config.ts)**: Provides `getAgentId`, 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.json`](https://github.com/letta-ai/claude-subconscious/blob/main/.letta/claude/conversations.json) using a schema that includes both `conversationId` and `agentId` to 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 `typeof` checks 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`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/session_start.ts) lines 31-38—the plugin deletes the stale conversation entry from [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/session_start.ts) or [`conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/.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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json); the plugin handles the string-to-object conversion transparently while preserving conversation continuity where possible.