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)
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.
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 |
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.
- Strict loader, regenerated bytes.
from_commonregenerates the fullevents.jsonl(header + turns) plussession.json,authority.json,commit.<gen>.json, and an emptycommit.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
Thinkingblocks are preserved in a privatetxcript-meta.jsonsidecar (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_allon edits (fx'sedit_filehas no such flag),terminalaction/profileand 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*.lockscratch are ephemeral tool-output/state and are not part of the session round trip.
- Observed from fx 0.0.5 sessions under
~/.fx/sessionson 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).