External Agent Session Import Mechanism and Ledger File in the Codex Plugin

The Codex plugin imports external Claude sessions into Codex threads using externalAgentSessionMigration() and requestExternalAgentSessionImport(), then caches results in a JSON ledger file at ~/.codex/external_agent_session_imports.json to prevent duplicate imports.

The external agent session import mechanism enables seamless migration of Claude conversation sessions into the Codex environment. This functionality, implemented in plugins/codex/scripts/lib/codex.mjs, relies on a persistent ledger file to track imported sessions and avoid redundant processing. Understanding both components is essential for developers integrating external agent workflows with Codex.

How External Agent Session Import Works

The import process follows a four-stage pipeline that transforms a source session file into a reusable Codex thread.

1. Building the Migration Payload

The externalAgentSessionMigration(sourcePath, cwd) function constructs a migration description that instructs Codex to create a new session from the specified file. This payload conforms to the standard migrationItems structure.

// From plugins/codex/scripts/lib/codex.mjs, lines 81-98
function externalAgentSessionMigration(sourcePath, cwd) {
  return {
    type: 'external_agent_session',
    source_path: sourcePath,
    working_directory: cwd,
    // Additional migration metadata...
  };
}

2. Requesting the Import

The requestExternalAgentSessionImport(client, params) function handles the actual communication with the Codex app-server:

// From plugins/codex/scripts/lib/codex.mjs, lines 101-130
async function requestExternalAgentSessionImport(client, params) {
  const originalHandler = client.handleNotification;
  
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      client.handleNotification = originalHandler;
      reject(new Error('Import timeout'));
    }, 30000);
    
    client.handleNotification = (method, params) => {
      if (method === 'EXTERNAL_AGENT_IMPORT_COMPLETED') {
        clearTimeout(timeout);
        client.handleNotification = originalHandler;
        resolve(params);
      }
    };
    
    client.sendRequest('externalAgentConfig/import', params);
  });
}

This function:

  • Temporarily replaces the client's notification handler
  • Sets a 30-second timeout for the operation
  • Resolves when EXTERNAL_AGENT_IMPORT_COMPLETED arrives
  • Rejects on timeout or error

3. Thread Initialization

After successful import, the plugin either starts a new thread via startThread() or resumes an existing one with resumeThread(). The import result may contain an imported_thread_id that links the external session to the Codex thread.

4. Ledger Caching

To prevent re-importing identical sessions, the plugin records the source file path, its SHA-256 hash, and the resulting thread ID in the ledger file.

The Ledger File: Structure and Purpose

The ledger file serves as a deterministic cache that enables thread reuse based on exact session file content.

Location

The ledger resides at:


$CODEX_HOME/external_agent_session_imports.json

If CODEX_HOME is unset, it defaults to ~/.codex. The path is assembled by importLedgerPath() in both production code and test fixtures.

File Structure

{
  "records": [
    {
      "source_path": "/home/user/project/claude_session.json",
      "content_sha256": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
      "imported_thread_id": "thread_12345"
    }
  ]
}

Each record contains:

  • source_path: Real-resolved absolute path of the imported file
  • content_sha256: SHA-256 hash of the file contents
  • imported_thread_id: Thread ID assigned by Codex after import

Lookup Operations

The importedThreadIdForSource(sourcePath) function (lines 61-78) performs ledger lookups:

// From plugins/codex/scripts/lib/codex.mjs
function importedThreadIdForSource(sourcePath) {
  const ledger = loadImportLedger();
  const normalizedPath = path.resolve(sourcePath);
  const contentHash = computeSha256(normalizedPath);
  
  const record = ledger.records
    .reverse()
    .find(r => r.source_path === normalizedPath && 
               r.content_sha256 === contentHash);
               
  return record?.imported_thread_id ?? null;
}

The function:

  • Normalizes the source path using path.resolve()
  • Computes the SHA-256 hash of file contents
  • Returns the most recent matching imported_thread_id
  • Returns null if no entry exists

Ledger Updates

When an import succeeds, saveImportLedger() appends a new record and persists the file. The test harness in tests/fake-codex-fixture.mjs (lines 57-61) emulates this behavior for validation.

Practical Implementation Examples

Complete Import Workflow

import { 
  withAppServer, 
  externalAgentSessionMigration, 
  requestExternalAgentSessionImport, 
  importedThreadIdForSource 
} from './lib/codex.mjs';

async function importClaudeSession(sourcePath, cwd) {
  const client = await CodexAppServerClient.connect(cwd);
  
  try {
    // 1. Check ledger for existing import
    const cachedThreadId = importedThreadIdForSource(sourcePath);
    if (cachedThreadId) {
      console.log('Reusing cached thread:', cachedThreadId);
      return cachedThreadId;
    }

    // 2. Build migration payload
    const migration = externalAgentSessionMigration(sourcePath, cwd);

    // 3. Request import from Codex server
    await requestExternalAgentSessionImport(client, migration);

    // 4. Start new thread for imported session
    const { thread } = await startThread(client, cwd);
    const threadId = thread.id;

    // 5. Ledger update happens automatically via plugin internals
    return threadId;
    
  } finally {
    await client.close();
  }
}

Ledger-Based Thread Reuse

import { importedThreadIdForSource } from './lib/codex.mjs';

function checkExistingImport(sessionPath) {
  const existingId = importedThreadIdForSource(sessionPath);
  
  if (existingId) {
    return {
      canReuse: true,
      threadId: existingId,
      action: 'resumeThread'
    };
  }
  
  return {
    canReuse: false,
    threadId: null,
    action: 'importAndStartThread'
  };
}

Key Source Files and Functions

Component Location Purpose
Migration builder plugins/codex/scripts/lib/codex.mjs:81-98 Creates externalAgentSessionMigration payloads
Import request handler plugins/codex/scripts/lib/codex.mjs:101-130 Manages externalAgentConfig/import RPC and notification waiting
Ledger lookup plugins/codex/scripts/lib/codex.mjs:61-78 importedThreadIdForSource() function
Ledger path utility tests/fake-codex-fixture.mjs:48-50 importLedgerPath() implementation
Ledger persistence tests/fake-codex-fixture.mjs:57-61 saveImportLedger() and loadImportLedger()
Thread start integration plugins/codex/scripts/lib/codex.mjs:1080-1085 Uses imported_thread_id when starting threads

Summary

  • The external agent session import mechanism uses externalAgentSessionMigration() to prepare payloads and requestExternalAgentSessionImport() to coordinate with the Codex app-server via the externalAgentConfig/import method.
  • The ledger file at ~/.codex/external_agent_session_imports.json caches mappings between source session files (by path and SHA-256 hash) and their resulting Codex thread IDs.
  • Thread reuse is determined by exact content matching—modifying a session file triggers a fresh import due to hash mismatch.
  • All core functionality resides in plugins/codex/scripts/lib/codex.mjs, with test coverage in tests/fake-codex-fixture.mjs.

Frequently Asked Questions

What triggers a new import versus reusing an existing thread?

A new import occurs when importedThreadIdForSource() finds no ledger entry matching both the resolved source path and the current SHA-256 hash of the file contents. If the file changes in any way—including whitespace or encoding—the hash mismatch forces re-import.

Where is the ledger file stored and can it be relocated?

The ledger file defaults to ~/.codex/external_agent_session_imports.json. It respects the CODEX_HOME environment variable, allowing relocation by setting CODEX_HOME to an alternative directory before running the plugin.

How does the plugin handle import failures or timeouts?

The requestExternalAgentSessionImport() function implements a 30-second timeout mechanism. If EXTERNAL_AGENT_IMPORT_COMPLETED does not arrive within this window, or if an error notification is received, the promise rejects and the original notification handler is restored. The ledger remains unchanged on failure, so subsequent attempts will retry the full import process.

Can multiple external agent sessions be imported into the same Codex thread?

The current implementation creates a distinct thread per imported session. The imported_thread_id in the ledger represents a one-to-one mapping between source session file and Codex thread. To merge multiple sessions, you would need to combine them externally before import or manage multiple thread IDs within your application logic.

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 →