txcript 0.12.0

Convert coding-agent session transcripts between harness formats.
Documentation
# fx

fx is Vercel's terminal coding agent — the `fx` binary (`fx.sh`), a native Rust
CLI that runs against Vercel's AI Gateway. txcript's mapping was
reverse-engineered from real sessions written by fx 0.0.5 on this machine; fx
is not open source and publishes no format spec, so the bytes on disk are the
authority.

```
~/.fx/sessions/                            (or $FX_HOME/sessions)
├── index.json, latest/, *.lock            (cross-session state; not carried)
└── <session-id>/                          one dir per session
    ├── events.jsonl        ── the append-only event log  (the conversation)
    ├── session.json        ── derived header + byte offsets
    ├── authority.json      ── session identity
    ├── commit.<gen>.json   ┐ the commit boundary
    ├── commit.lock         ┘ (empty marker)
    ├── display.json        ── title/preview for `fx sessions`
    ├── usage-v2.json, checkpoint.json, images/*.bin   (carried when present)
    └── subagent/, artifacts/, logs/, *.lock           (ephemeral; not carried)
```

## On disk

A session is a **directory of plain files**, not a single transcript file. The
root is `~/.fx/sessions`, overridden by `$FX_HOME` (sessions then live at
`$FX_HOME/sessions`). Unlike most harnesses there is no per-workspace subdir:
the session directory sits directly under the root, named by the fx session id
(`<ms>-<ns>-<16 hex>`). txcript's discovery walks that one level and sniffs a
session by an `events.jsonl` whose first record is a `session_started` event —
the sibling `index.json`, `latest/`, and `relationship-migration-index.json`
are skipped. Resume is by exact id (`fx --resume <id>` / `fx ask --resume
<id>`); it reads the session directory directly and does not need the global
`index.json`.

## Dissection of a transcript

The conversation lives entirely in `events.jsonl`, an append-only log of typed
events. Every event shares one envelope — `schema_version`, `log_generation`,
`seq`, `event_id`, `timestamp_ms`, `kind`, `payload` — and fx's loader is
strict: an unknown event `kind`, an unknown key at any level, a non-string tool
`output`, or a null committed `assistant` fails the whole session load.

| Their name | What it is | Maps to |
|---|---|---|
| `events.jsonl` event, `kind: "session_started"` | The required header: id, workspace, `preferences` (model/effort/provider), a zeroed usage snapshot | `Meta`; regenerated on write |
| `kind: "history_turn_committed"` | One committed turn (see below) | The conversation |
| `kind: "recovery_checkpoint_set"`, `"usage_checkpointed"` | Mid-turn recovery and token bookkeeping | Skipped (kept in the native body) |
| turn `user` | Prompt `text` plus `images` (each an `images/*.bin` snapshot with `media_type` + `snapshot_sha256`) | `Role::User` message: `Text` + `Image` blocks (bytes read from `images/`) |
| turn `execution.tool_steps[]` | Each step: an intermediate `assistant` text, its `tool_calls` (`arguments_json` is JSON-in-a-string), and `tool_results` (`status` `success`/`failure`) | An assistant message (`Text` + `ToolUse`) and the paired `ToolResult` user message; `status: "failure"` sets `is_error` |
| turn `assistant` | The concluding reply text | The turn's final assistant `Text`, stamped `StopReason::EndTurn` |
| `kind: "interrupted"` turn | A cancelled turn: a single in-flight `tool_call`, no result, `terminal_reason` | Assistant message with the pending `ToolUse`, stamped `StopReason::Aborted` |
| `session.json` | Derived header: id, workspace, model, and the `event_log_bytes` / `generation_base_bytes` byte offsets into the log | `Meta`; regenerated with recomputed offsets |
| `display.json` | Title/preview | `Meta.title` |

### Tools

fx's file/shell tools map onto the Claude convention: `read_file`→`Read`,
`write_file`→`Write`, `edit_file`→`Edit`, `terminal`→`Bash` (its `path` keys
become `file_path`, `cwd` becomes `workdir`, and the fx-only `action`/`profile`
are dropped). Everything else — `grep_files`, `glob_files`, `vision`,
`semantic_search`, `mcp_*`, … — passes through as `Tool::Raw` under its native
name. `from_common` inverts the mapping, synthesizing the `action`/`profile`/
`cwd` fx expects.

## Caveats

- **Strict loader, regenerated bytes.** `from_common` regenerates the full
  `events.jsonl` (header + turns) plus `session.json`, `authority.json`,
  `commit.<gen>.json`, and an empty `commit.lock`, recomputing the byte offsets
  the header and commit record pin. Missing any of these makes fx report the
  session corrupt or its authority unavailable.
- **Reasoning has no native slot.** fx's committed-turn schema has no field for
  model reasoning and rejects unknown keys, so `Thinking` blocks are preserved
  in a private `txcript-meta.json` sidecar (keyed by turn and assistant-message
  index) that fx ignores. Same-harness round trips keep reasoning; fx resume
  renders the conversation without it.
- **Representational losses through `Common`.** Per-turn token usage, per-message
  model (fx stores one session model), `replace_all` on edits (fx's `edit_file`
  has no such flag), `terminal` `action`/`profile` and other non-Bash tool
  argument shapes when a session *leaves* fx, and the recovery-checkpoint state.
  `execution.files` (fx's changed-file review panel) is regenerated empty; the
  conversation itself is intact.
- **Not carried.** `subagent/`, `artifacts/`, `logs/`, and `*.lock` scratch are
  ephemeral tool-output/state and are not part of the session round trip.

## References

- Observed from fx 0.0.5 sessions under `~/.fx/sessions` on macOS.
- Docs: <https://fx.sh/docs> (behavioral, not a format spec).
- Parser: `src/harness/fx.rs`; tests: `tests/integration/fx.rs`.

Last verified: 2026-08-24 (fx 0.0.5).