中文 Books

Chapter 19: External Migration and Backward Compatibility

Reading Contract: Use this chapter to answer one question: how can Codex import external agent configuration and history without inheriting the external runtime’s semantics? Track four owners: detection, conversion, native runtime refresh, and session-import ledger. Afterward, you should be able to distinguish a native artifact, an imported artifact, a skipped external construct, and a compatibility bridge.

Compatibility lanes showing external config migration, session import, adapters, and legacy state protection
Compatibility lanes let Codex import older or foreign shapes while protecting the native runtime model from inherited ambiguity.

Source boundary: named files, structs, functions, protocol request/response shapes, and import records are verified source only where this chapter links to the pinned Codex commit or this chapter’s Source Map. The broader rule that migration is “conservative translation” is surrounding contract inference from those visible conversion and ledger boundaries. This chapter does not claim to emulate any external agent’s private runtime.

Chapter 18 separated Codex-native extension planes into skills, plugins, connectors, and typed contributors. This chapter asks what happens when users arrive with artifacts that were not born inside those planes: external configs, commands, hooks, subagents, MCP servers, and JSONL sessions.

You are here: Codex can load native extension surfaces with explicit trust boundaries.

Problem: users bring useful external state, but that state may use different prompt rules, hook timing, command expansion, permissions, session schemas, and trust assumptions.

Mental model: migration is a one-way adapter into native Codex artifacts plus provenance; it is not a hidden compatibility mode inside every later turn.

Backward compatibility in an agent system is not only about accepting old API fields. It is also about accepting user history and local automation without allowing ambiguous behavior to become hidden authority. The migration layer should read a source artifact, recognize supported constructs, translate them into native shapes, skip unsupported or unsafe cases, and record enough metadata for users and tests to understand the result.

The invariant: after migration, the turn loop should run Codex-native tools, hooks, skills, MCP configs, and rollout history. It should not keep asking “what would the external runtime have done?“

1. Migration Starts As Detection, Not Mutation

The source exposes migration as typed items. ExternalAgentConfigMigrationItemType includes config, skills, AGENTS.md, plugins, MCP server config, subagents, hooks, commands, and sessions. Detection returns ExternalAgentConfigMigrationItem records with a description, optional cwd, and optional details.

Shape-level:

{
  "item_type": "McpServerConfig",
  "description": "Import external MCP server configuration",
  "cwd": "/project",
  "details": {
    "mcp_servers": [{ "name": "github" }],
    "commands": [],
    "sessions": []
  }
}

That is a detection report, not a mutation. The request processor’s detect() maps core migration items into protocol response items for the app-server client. The client can then ask to import selected items.

1.1 Import Has Foreground And Background Phases

ExternalAgentConfigRequestProcessor::import() shows the runtime shape:

selected migration items
  -> validate pending session imports
  -> import config-like items
  -> refresh runtime config when needed
  -> send import response
  -> background plugin/session imports
  -> clear plugin/skill caches if plugin imports ran
  -> notify import completed

This split is important. Some imports produce immediate local config changes. Some plugin and session work can continue in the background. The processor sends a response before background imports finish, then emits ExternalAgentConfigImportCompleted when the remaining work ends. A migration UI therefore should not treat “request accepted” as “every plugin and session is already imported.”

2. Config Migration Is Translation Into Native Shapes

External configuration migration handles several artifact families:

Source artifactNative destinationVerified source surfaceConservative rule
MCP server entriesCodex MCP configbuild_mcp_config_from_external()import supported server records and preserve merge rules
hooksCodex hook configimport_hooks()copy only representable hook mappings and scripts
commandsskills/workflow unitsimport_commands()require parseable documents and bounded metadata
subagentsnative agent definitionsimport_subagents()require stable frontmatter and safe target names
pluginsplugin config/storepending plugin import pathsplit local and remote import work

The converter source makes the conservative style concrete. build_mcp_config_from_external() reads external MCP server maps, applies enabled/disabled server lists, and returns a TOML table rooted at mcp_servers. import_commands() creates skill directories only for supported, unique command sources and skips existing targets. import_subagents() does the same for agent files.

Shape-level, the boundary is lossy by design:

external command document
  -> parse frontmatter and body
  -> derive stable skill name and description
  -> skip if target already exists
  -> write native SKILL.md

external dynamic behavior
  -> no safe native representation
  -> skip and report

The importer should never guess a permission boundary. If a source command relies on dynamic provider-specific expansion that Codex cannot represent, the safe import result is “skipped”, not “smuggled into a shell hook.”

3. Session Import Preserves History As Evidence

Session import is different from config migration because history is not a future capability. It is evidence of past conversation. The session importer has to translate enough of an external JSONL transcript into Codex rollout items that a thread can be listed, resumed, and audited, while still marking the source as imported.

History surface fanout showing durable transcript, model projection, UI history, resume reconstruction, fork baseline, and audit evidence
Imported history must choose which surface it is rebuilding: transcript evidence, model projection, UI history, or resume baseline.

The verified lifecycle is:

  1. Detection summarizes candidate sessions only when records contain a cwd, at least one user/assistant message, and a latest timestamp. summarize_session() builds that summary.
  2. Import validation canonicalizes selected paths and rejects sessions outside the detected external projects root. validate_pending_session_imports() also deduplicates selected canonical paths.
  3. prepare_validated_session_imports() skips sessions already recorded in the content ledger and loads only importable sessions whose cwd still exists. load_importable_session() is the visible cwd check.
  4. The request processor imports the session by starting a new Codex thread with InitialHistory::Forked(rollout_items), then optionally normalizes the title into thread metadata.
  5. After success, record_imported_session() writes source path, content SHA-256, imported thread ID, and timestamp into a ledger.

3.1 The JSONL Shape Changes At The Boundary

The parser is intentionally selective. conversation_message_from_record() accepts only user/assistant records, skips meta and sidechain records, extracts message content, and parses timestamps. extract_message_text() turns text blocks into text, tool-use blocks into notes, tool-result blocks into notes, ignores thinking blocks, and labels unsupported blocks.

Shape-level:

{
  "type": "user",
  "cwd": "/project",
  "message": {
    "content": [
      { "type": "text", "text": "Fix the parser" },
      { "type": "tool_result", "content": "long external output..." }
    ]
  },
  "isSidechain": false
}

becomes a Codex-importable conversation message such as:

{
  "role": "user",
  "text": "Fix the parser\n[external tool result: long external output...]",
  "timestamp": "parsed timestamp if present"
}

This is a shape-level example, not the exact serialized rollout record. The key point is the invariant: imported history is useful as history, but it is not treated as a lossless replay of another runtime’s internal state.

Replay, resume, and fork path reconstructing a thread from stored history, forked items, rollback state, and continuation baseline
Session import is safe only when imported items become a forked native baseline rather than a foreign runtime replay.

3.2 The Ledger Is Content-Based, Not Just Path-Based

The session ledger protects idempotency without hiding changed source content. ImportedExternalAgentSessionRecord stores source path, content_sha256, imported thread ID, and import time. contains_current_source() recomputes the current file hash before deciding that a source has already been imported.

That avoids two bad extremes:

Ledger designFailure
path onlychanged JSONL content is suppressed forever
no ledgerevery migration prompt can duplicate the same session
path + content hashunchanged sessions are skipped, changed sessions can be detected again

4. Compatibility Bridges Should End At The Boundary

Migration is one bridge. Protocol compatibility is another. Earlier chapters introduced generated schemas, v1/v2 protocol coexistence, request aliases, experimental gates, and client-version behavior. Chapter 19’s general rule is that compatibility should be explicit about what it bridges and where it stops.

BridgeWhat it protectsWhat it must not do
schema aliasesolder clients and stored eventshide incompatible semantics
experimental gatesunstable capabilitiesmake unstable fields look permanent
migration convertersuser workflows from other toolsemulate another runtime forever
import ledgersidempotent session importsuppress changed source content
provenance markersauditability of imported historypollute model context with private implementation detail

codex-rs/app-server-protocol/src/protocol/mod.rs is a small but useful reminder: protocol compatibility is organized as a namespace with common pieces, mappers, thread history, and v1/v2 modules. Compatibility belongs at the protocol edge and migration edge, not scattered through core logic as “maybe this was an external thing.”

5. Failure Conditions Are The Product Contract

Migration fails most dangerously when it succeeds silently but changes meaning. The user needs a report, and the runtime needs clear failure categories.

FailureBoundary ownerCorrect behavior
unsupported command shapecommand converterskip and report, do not invent execution semantics
existing native targetimporterpreserve existing Codex file
invalid external JSONLsession parserskip record/session rather than corrupt rollout history
cwd no longer existssession import preparationdo not import a thread with unusable working context
stale session ledgerledger hash checkrecompute content hash before skipping
background plugin failureimport processorwarn and still send completion notification after background phase
config-like import ranrequest processorrefresh runtime config and clear relevant caches

The product contract is not “all external state becomes native.” It is “supported state becomes native, unsupported state remains outside with an explanation.”

Trace Ledger

QuestionChapter 19 answer
Where is the user request now?It can be backed by imported native config, imported skills/hooks/MCP entries, background plugin imports, or imported rollout history.
What carries it?migration items, migration details, converted native artifacts, app-server import responses, InitialHistory::Forked, thread metadata patches, and the content-hash session ledger.
Who owns the next decision?Detection reports candidates; converters decide supported native shapes; the request processor refreshes runtime config and runs background imports; native loaders own the result afterward.
What must remain invariant?Migration must not overwrite native user work, inherit trust blindly, emulate another runtime forever, or treat external history as a lossless native replay.
What can fail here?unsupported semantics, disabled/duplicate entries, existing target conflicts, invalid JSONL, missing cwd, stale or invalid ledger data, remote plugin errors, or source paths outside the detected import boundary.

Apply This

  1. Detect before mutating. Use this for external state. Return typed migration items with descriptions before writing native files. Pitfall: silent migration that users cannot audit.
  2. Translate into native contracts. Use this for commands, hooks, subagents, MCP servers, and sessions. Convert only supported shapes. Pitfall: preserving foreign runtime assumptions as hidden behavior.
  3. Record lossy boundaries. Use this when importing histories. Keep provenance and ledger data, and make unsupported blocks explicit. Pitfall: pretending external JSONL is native rollout replay.
  4. Make idempotency content-aware. Use this for imports that can be repeated. Hash source content, not just paths. Pitfall: suppressing changed sessions or duplicating unchanged ones.
  5. End the bridge after import. Use this for compatibility code. Let native loaders own migrated artifacts after conversion. Pitfall: spreading compatibility branches through the core turn loop.

What Comes Next

Part V ends with a runtime that can consume external tools, load native extension packages, and migrate outside state into native contracts. Chapter 20 turns to coordination beyond one agent: multi-agent threads, graph edges, live status, and trace reconstruction.