Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ here. Native format compatibility is documented separately in
- Read root Codex rollouts written with `history_mode: "paginated"` by using
canonical completed user and agent turn items while keeping provider
envelopes out of the portable transcript.
- Reject paginated history bases, subagent projections, mixed or conflicting
- Support complete self-contained paginated subagent projections using primary
child metadata and the exact ordinal boundary; exclude inherited parent
context, preserve readable targeted correspondence, and account for encrypted
fragments without restoring live multi-agent orchestration. Catalog discovery
agrees with the bounded reader.
- Reject external paginated history bases, invalid/incomplete child projections, mixed or conflicting
history modes, unknown modes, and non-contiguous ordinals instead of
guessing at a resumable conversation.
- Add a sanitized real-structure Codex 0.153.4 paginated fixture and regressions
Expand Down
27 changes: 23 additions & 4 deletions docs/format-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,24 @@ reasoning data. Direct image/audio user inputs are retained when represented
by the canonical TurnItem. Local media paths and other TurnItem blocks remain
explicit opaque losses.

Self-contained paginated subagent rollouts are also supported. The first session
metadata identifies the child and declares `subagent_history_start_ordinal`.
Every ordinal must remain contiguous from zero and the entire inherited prefix
must be present. Parent records below the boundary, including inherited metadata,
messages, tools, and compactions, are excluded and counted explicitly. Child
tools/results retain their linkage; a result referring only to an inherited
parent call is excluded rather than synthesized as child activity.

Readable `agent_message` fragments addressed exactly to the child's `agent_path`
are retained as ordinary user messages, without system/developer privileges.
Encrypted fragments and envelope/routing metadata are explicit omissions.
Foreign recipients and ambiguous envelopes remain opaque; malformed targeted
blocks fail closed. Duplicate suppression requires an authoritative completed
message with the same ID, role, and exact text. No decryption is attempted, and
partial plaintext does not establish recovery of the complete task payload.
The result is an independent conversation, not restored live parent/child
orchestration. Catalog discovery applies the same bounded reader checks.

Current legacy rollouts can contain `compacted.replacement_history`. Codex
installs that array as the effective history at the checkpoint and replays only
later items. In a bounded structural audit, replacement arrays ended in a
Expand All @@ -312,14 +330,14 @@ Those records are not all portable conversation history.

Every ordered pair among the eighteen formats is implemented, for 324 routes:

- full portable adapters: Claude, Codex legacy and root paginated, Pi, OMP, OpenCode, Copilot,
- full portable adapters: Claude, Codex legacy and self-contained paginated, Pi, OMP, OpenCode, Copilot,
Antigravity, Vibe, Muse, Qwen, Kimi, Grok, Kilo, OpenHands, Hermes,
MastraCode, and Devin;
- experimental text-only adapter: Cursor.

Same-format routes are portable rewrites into new sessions, not byte copies.
Codex `history_base` lineage and paginated subagent projections remain
fail-closed. Cursor is experimental, build-pinned, and deliberately transfers
Codex external `history_base` lineage remains fail-closed. Incomplete or invalid
child projection boundaries are also rejected. Cursor is experimental, build-pinned, and deliberately transfers
only ordered user/assistant text. The
detailed table below explains the original Claude/Codex pair; target-specific
behavior is documented in [Additional native formats](additional-target-formats.md)
Expand Down Expand Up @@ -361,7 +379,8 @@ Legend:
| Claude sidechains/subagents | **Unsupported** | N/A | The catalog indexes nested sidechains as unsupported, but direct lookup/conversion does not import them; transfer the parent session. |
| Codex legacy linear history | N/A | **Supported** | Ordered response items become one linear Claude UUID graph. |
| Codex root paginated history | N/A | **Supported** | Contiguous canonical completed TurnItems supply user/assistant turns; provider-context messages are not replayed. Replacement-history compaction uses the expanded-transcript policy above. |
| Codex paginated forks/subagents | N/A | **Unsupported** | `history_base` and `subagent_history_start_ordinal` require external or projected history and are rejected rather than silently truncating or duplicating a conversation. |
| Codex self-contained paginated subagents | N/A | **Supported** | Complete ordinal prefixes are validated; inherited parent context is excluded and readable child correspondence is retained with explicit private-state omissions. Imports are independent conversations. |
| Codex external history bases/forks | N/A | **Unsupported** | `history_base` requires external lineage resolution and is rejected rather than silently truncating a conversation. |
| Codex UI-only messages | N/A | **Lossy fallback** | Used as the conversation when no response-item messages exist. In a mixed partial file, exact normalized duplicates are removed and unmatched projections are retained with `message:ui_only_projection`; fuzzy matching is never used. |
| Turn context, policies, world state, snapshots | **Unsupported** | **Unsupported** | Codex `turn_context` is counted as context; `world_state` and `security_risk_score` become counted opaque events. Shell snapshots, approvals, external credential stores, MCP state, memories, goals, and configuration are outside transcript conversion. |
| Unknown source records/blocks | **Unsupported** | **Unsupported** | They become content-free opaque/sentinel events where recognized and are counted at write time, including unknown nested tool-result blocks. |
Expand Down
11 changes: 7 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,13 @@ credentials. See the [Hermes](hermes-format.md),

A complete root paginated rollout is supported when every record has an integer
ordinal contiguous from zero. Missing/gapped ordinals are treated as corrupt.
`history_base` and `subagent_history_start_ordinal` mean the file depends on an
external or projected prefix and remain unsupported; `--format codex` cannot
bypass either guard. Resume or export the root session instead of flattening
the dependent file by hand.
Self-contained child rollouts with `subagent_history_start_ordinal` are supported
when their inherited prefix is complete. The reader excludes that prefix and
retains the child's projected history and readable incoming correspondence.
Encrypted message fragments remain explicit omissions. Invalid boundaries,
incomplete ordinal streams, and external `history_base` lineage are rejected;
`--format codex` cannot bypass those checks. An imported child is an independent
conversation and does not recreate live multi-agent orchestration.

## Claude sidechain/subagent

Expand Down
1 change: 1 addition & 0 deletions scripts/run-native-test-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ case "$client" in
tests/test_additional_formats_native.py::test_opencode_11720_official_import_and_loopback_resume
tests/test_additional_formats_native.py::test_opencode_cli_import_uses_official_importer_and_rejects_native_collision
tests/test_additional_formats_native.py::test_opencode_native_replay_preserves_source_order_with_decreasing_timestamps
tests/test_codex_subagent_projection.py::test_pinned_legacy_client_accepts_child_projection_without_parent_context
'tests/test_opencode_kilo_corpus_native.py::test_exact_client_captures_from_empty_through_public_surfaces[opencode]'
'tests/test_opencode_kilo_corpus_native.py::test_exact_client_cold_import_export_and_continuation_preserve_prefix[opencode]'
)
Expand Down
42 changes: 40 additions & 2 deletions src/session_migrate/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,7 @@ def _scan_file(path: Path, agent_format: AgentFormat, root: Path) -> _Scan:
history_mode = None
history_base = False
codex_subagent_history = False
codex_child_start: int | None = None
codex_ordinals_complete = True
codex_history_mode_conflict = False
codex_selected_history_mode = None
Expand Down Expand Up @@ -2130,7 +2131,22 @@ def _scan_file(path: Path, agent_format: AgentFormat, root: Path) -> _Scan:
payload = value.get("payload")
if not isinstance(payload, dict):
payload = {}
if (
codex_child_start is not None
and records > 1
and isinstance(ordinal, int)
and ordinal < codex_child_start
):
continue
if record_type == "session_meta":
if records == 1:
boundary = payload.get("subagent_history_start_ordinal")
if (
isinstance(boundary, int)
and not isinstance(boundary, bool)
and boundary >= 0
):
codex_child_start = boundary
has_session_meta = True
session_id = session_id or _normalized_uuid(
_string(payload.get("id")) or _string(payload.get("session_id"))
Expand Down Expand Up @@ -2295,10 +2311,32 @@ def _scan_file(path: Path, agent_format: AgentFormat, root: Path) -> _Scan:
elif agent_format == AgentFormat.CODEX:
if not has_session_meta:
status, reason = "corrupt", "missing_session_meta"
elif codex_subagent_history:
# The bounded reader owns child metadata/projection validation.
# Parsed content is transient and never stored in the catalog.
try:
child = codex.parse(path)
except SessionMigrateError as exc:
if "history_base" in str(exc):
status, reason = "unsupported", "codex_history_base"
elif "history mode" in str(exc) and "not supported" in str(exc):
status, reason = "unsupported", "codex_history_mode"
else:
status, reason = "corrupt", "codex_subagent_projection"
else:
if not any(
e.kind
in {
EventKind.MESSAGE,
EventKind.TOOL_CALL,
EventKind.TOOL_RESULT,
EventKind.COMPACTION,
}
for e in child.events
):
status, reason = "corrupt", "no_conversation_records"
elif history_base:
status, reason = "unsupported", "codex_history_base"
elif codex_subagent_history:
status, reason = "unsupported", "codex_subagent_history"
elif codex_history_mode_conflict:
status, reason = "corrupt", "codex_history_mode_conflict"
elif (
Expand Down
Loading
Loading